First implementation of a book uploader
This commit is contained in:
parent
e2a35571cf
commit
f1cba399af
14 changed files with 8220 additions and 335 deletions
|
@ -236,6 +236,31 @@ class ClientApiController {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createBook({request, response, auth}) {
|
||||||
|
// TODO: Validate input!
|
||||||
|
const user = auth.user;
|
||||||
|
const bookPayload = request.body;
|
||||||
|
// console.log('BookPages')
|
||||||
|
const bookHash = uuidv4();
|
||||||
|
const bookDrivePromises = [];
|
||||||
|
const bookRelativePath = `uploads/${bookHash}`;
|
||||||
|
const bookAbsolutePath = `books/${bookRelativePath}`;
|
||||||
|
for (let i = 0; i < bookPayload.pages.length; i++) {
|
||||||
|
const filePayload = bookPayload.pages[i];
|
||||||
|
bookDrivePromises.push(FileUtils.saveBase64File(
|
||||||
|
filePayload, `${bookAbsolutePath}/${i + 1}.jpg`));
|
||||||
|
};
|
||||||
|
await Promise.all(bookDrivePromises);
|
||||||
|
const book = await Book.create({
|
||||||
|
user_id: user.id,
|
||||||
|
title: bookPayload.title,
|
||||||
|
pages: bookPayload.pages.length,
|
||||||
|
book_folder: bookRelativePath,
|
||||||
|
ltr: bookPayload.ltr
|
||||||
|
});
|
||||||
|
return {code: 0, data: book};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientApiController
|
module.exports = ClientApiController
|
||||||
|
|
|
@ -2,11 +2,12 @@ const Drive = use('Drive');
|
||||||
|
|
||||||
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
static async saveBase64File(base64Str) {
|
static async saveBase64File(base64Str, _fileName = null) {
|
||||||
console.log(base64Str.length);
|
console.log(base64Str.length);
|
||||||
const parsed = parseBase64(base64Str);
|
const parsed = parseBase64(base64Str);
|
||||||
const fileName =
|
const fileName = _fileName ||
|
||||||
`${Date.now()}-${Math.random() * 1000}.${parsed.extension}`;
|
`${Date.now()}-${Math.random() * 1000}.${parsed.extension}`;
|
||||||
|
console.log(fileName);
|
||||||
const file = await Drive.put(fileName, parsed.data);
|
const file = await Drive.put(fileName, parsed.data);
|
||||||
return {fileName, file};
|
return {fileName, file};
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -7886,6 +7886,12 @@ html {
|
||||||
.is-fullheight {
|
.is-fullheight {
|
||||||
min-height: calc(100vh - ( 3.25rem )); }
|
min-height: calc(100vh - ( 3.25rem )); }
|
||||||
|
|
||||||
|
.is-fullwidth-container {
|
||||||
|
width: 100%; }
|
||||||
|
|
||||||
|
.is-fullheight-container {
|
||||||
|
height: 100%; }
|
||||||
|
|
||||||
.is-fullwidth {
|
.is-fullwidth {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
overflow: hidden; }
|
overflow: hidden; }
|
||||||
|
@ -8003,11 +8009,18 @@ video {
|
||||||
height: 100%; }
|
height: 100%; }
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .2s; }
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
position: absolute; }
|
||||||
|
|
||||||
|
.fade-enter-active {
|
||||||
|
transition-delay: 0.2s; }
|
||||||
|
|
||||||
.fade-enter, .fade-leave-to {
|
.fade-enter, .fade-leave-to {
|
||||||
opacity: 0; }
|
opacity: 0; }
|
||||||
|
|
||||||
|
.fade-enter-to, .fade-leave {
|
||||||
|
opacity: 1; }
|
||||||
|
|
||||||
.slide-left-enter-active,
|
.slide-left-enter-active,
|
||||||
.slide-left-leave-active,
|
.slide-left-leave-active,
|
||||||
.slide-right-enter-active,
|
.slide-right-enter-active,
|
||||||
|
@ -8017,6 +8030,10 @@ video {
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
overflow: hidden; }
|
overflow: hidden; }
|
||||||
|
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-right-enter-active {
|
||||||
|
transition-delay: 0.2s; }
|
||||||
|
|
||||||
.slide-left-enter,
|
.slide-left-enter,
|
||||||
.slide-right-leave-active {
|
.slide-right-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -8036,6 +8053,9 @@ video {
|
||||||
.is-justify-centered {
|
.is-justify-centered {
|
||||||
justify-content: center; }
|
justify-content: center; }
|
||||||
|
|
||||||
|
.is-justify-between {
|
||||||
|
justify-content: space-between; }
|
||||||
|
|
||||||
.video-side-bar {
|
.video-side-bar {
|
||||||
height: calc(100vh - ( 3.25rem )); }
|
height: calc(100vh - ( 3.25rem )); }
|
||||||
.video-side-bar video {
|
.video-side-bar video {
|
||||||
|
@ -8085,3 +8105,34 @@ video {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
justify-content: space-between; }
|
justify-content: space-between; }
|
||||||
|
|
||||||
|
.page-editor .croppa-container {
|
||||||
|
border: 2px solid whitesmoke;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: white; }
|
||||||
|
|
||||||
|
.edit-page-controllers {
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: rgba(134, 134, 134, 0.1); }
|
||||||
|
|
||||||
|
.book-uploading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 100; }
|
||||||
|
|
||||||
|
.book-uploading-overlay-content {
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 600px; }
|
||||||
|
.book-uploading-overlay-content .title {
|
||||||
|
color: whitesmoke;
|
||||||
|
opacity: 1; }
|
||||||
|
.book-uploading-overlay-content .subtitle {
|
||||||
|
color: whitesmoke;
|
||||||
|
opacity: 0.8; }
|
||||||
|
|
|
@ -221,6 +221,12 @@ $sizes: (
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
.is-fullwidth-container{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
.is-fullheight-container{
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
.is-fullwidth{
|
.is-fullwidth{
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -368,11 +374,19 @@ video{
|
||||||
}
|
}
|
||||||
//Fade vue transition
|
//Fade vue transition
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .2s;
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
.fade-enter-active{
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.fade-enter, .fade-leave-to{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
.fade-enter-to, .fade-leave{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.slide-left-enter-active,
|
.slide-left-enter-active,
|
||||||
.slide-left-leave-active,
|
.slide-left-leave-active,
|
||||||
|
@ -383,6 +397,10 @@ video{
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-right-enter-active{
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-left-enter,
|
.slide-left-enter,
|
||||||
.slide-right-leave-active {
|
.slide-right-leave-active {
|
||||||
|
@ -405,6 +423,10 @@ video{
|
||||||
.is-justify-centered{
|
.is-justify-centered{
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.is-justify-between{
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.video-side-bar{
|
.video-side-bar{
|
||||||
height: calc(100vh - ( #{$navbar-height} ) );
|
height: calc(100vh - ( #{$navbar-height} ) );
|
||||||
|
@ -465,3 +487,41 @@ video{
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//edit page
|
||||||
|
.page-editor{
|
||||||
|
|
||||||
|
.croppa-container {
|
||||||
|
border: 2px solid whitesmoke;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-page-controllers{
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: rgba(134, 134, 134, 0.1);
|
||||||
|
}
|
||||||
|
.book-uploading-overlay{
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba($color: #000000, $alpha: .6);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.book-uploading-overlay-content{
|
||||||
|
position: relative;
|
||||||
|
top:50%;
|
||||||
|
left:50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 600px;
|
||||||
|
.title{
|
||||||
|
color: whitesmoke;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.subtitle{
|
||||||
|
color: whitesmoke;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Home from "../views/home.vue";
|
||||||
import Settings from "../views/settings.vue";
|
import Settings from "../views/settings.vue";
|
||||||
import Call from "../views/call.vue";
|
import Call from "../views/call.vue";
|
||||||
import ChildProfile from "../views/child_profile.vue";
|
import ChildProfile from "../views/child_profile.vue";
|
||||||
|
import EditBook from "../views/edit_book.vue";
|
||||||
|
|
||||||
// Call Views
|
// Call Views
|
||||||
import CallLobby from "../views/call_views/Lobby.vue";
|
import CallLobby from "../views/call_views/Lobby.vue";
|
||||||
|
@ -30,6 +31,10 @@ const routes: RouteConfig[] = [
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
component: Settings
|
component: Settings
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/create/book",
|
||||||
|
component: EditBook
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/call/:id",
|
path: "/call/:id",
|
||||||
component: Call,
|
component: Call,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<div :class="`book-view ${flipbookRef ? '' : 'is-transparent'}`" v-else>
|
<div :class="`is-fullheight-container ${flipbookRef ? '' : 'is-transparent'}`" v-else>
|
||||||
<div class="book-view m-sm m-r-md">
|
<div class="book-view m-sm m-r-md">
|
||||||
<div
|
<div
|
||||||
class="go-left m-r-sm"
|
class="go-left m-r-sm"
|
||||||
|
|
581
resources/scripts/applications/home/views/edit_book.vue
Normal file
581
resources/scripts/applications/home/views/edit_book.vue
Normal file
|
@ -0,0 +1,581 @@
|
||||||
|
<template>
|
||||||
|
<div class="container is-fullwidth is-fullheight-container">
|
||||||
|
<transition name="fade">
|
||||||
|
<div class="book-uploading-overlay" v-if="uploading">
|
||||||
|
<div class="section book-uploading-overlay-content has-text-centered">
|
||||||
|
<h1 class="title">Uploading...</h1>
|
||||||
|
<h1 class="subtitle">Be pacient, This can take a while</h1>
|
||||||
|
<progress class="progress is-small is-primary" max="100">15%</progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<div class="columns is-flex is-fullheight-container">
|
||||||
|
<div class="column is-3">
|
||||||
|
<!-- Properties -->
|
||||||
|
<div class="card m-b-md">
|
||||||
|
<div class="card-content">
|
||||||
|
<h1 class="subtitle">
|
||||||
|
<i class="fa fa-book"></i> Properties
|
||||||
|
</h1>
|
||||||
|
<div class="book-properties">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Book Title:</label>
|
||||||
|
<input class="input" type="text" placeholder="My Book" v-model="book.title" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Book Author:</label>
|
||||||
|
<input class="input" type="text" placeholder="Savta Cochi" v-model="book.author" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Language Direction:</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
class="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="book.ltr"
|
||||||
|
aria-label="Book direction"
|
||||||
|
/>
|
||||||
|
{{book.ltr ? "Left To Right" : "Right To Left"}}
|
||||||
|
<i
|
||||||
|
:class="`fa fa-fw fa-arrow-${book.ltr?'right':'left'}`"
|
||||||
|
></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Total page count: #{{pages.length}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<a class="card-footer-item is-success" @click="onUploadClicked()">
|
||||||
|
<i class="fa fa-fw fa-upload"></i> Upload Book
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<!-- Pages -->
|
||||||
|
<div class="card">
|
||||||
|
<aside class="menu card-content">
|
||||||
|
<p class="menu-label">Pages</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li v-for="page in pages" :key="page.id">
|
||||||
|
<a
|
||||||
|
:class="{'is-active' : currentPage === page.id}"
|
||||||
|
@click="onPageClicked(page.id)"
|
||||||
|
>{{page.text}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<a class="card-footer-item" @click="onAddPageClicked()">
|
||||||
|
<i class="fa fa-fw fa-plus"></i> Add Page
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-9">
|
||||||
|
<div class="card is-fullheight-container bg-flower">
|
||||||
|
<div class="card-content is-fullheight-container">
|
||||||
|
<div class="tabs-container has-text-centered m-b-lg">
|
||||||
|
<div class="tabs is-centered">
|
||||||
|
<ul>
|
||||||
|
<li :class="editMode ? 'is-active' : ''" @click="editMode=true">
|
||||||
|
<a>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li :class="!editMode ? 'is-active' : ''" @click="editMode=false">
|
||||||
|
<a>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span>Preview</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content is-fullheight-container">
|
||||||
|
<!-- Book Edit -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div class="edit" v-if="editMode">
|
||||||
|
<div class="has-text-centered" v-if="currentPage < -1">
|
||||||
|
<h1 class="subtitle m-t-xl m-b-xl">This is exciting...</h1>
|
||||||
|
<a class="button is-large is-rounded" @click="onAddPageClicked()">
|
||||||
|
<i class="fa fa-fw fa-plus"></i> Add Cover
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="page-editor" v-else>
|
||||||
|
<div class="columns">
|
||||||
|
<!-- Croppa -->
|
||||||
|
<div class="column is-flex">
|
||||||
|
<div v-if="pages[currentPage].loaded">
|
||||||
|
<h1 class="subtitle">{{pages[currentPage].text}}</h1>
|
||||||
|
<croppa
|
||||||
|
v-model="pages[currentPage].croppa"
|
||||||
|
:prevent-white-space="true"
|
||||||
|
:show-remove-button="false"
|
||||||
|
:initial-image="pages[currentPage].image"
|
||||||
|
:width="croppaWidth"
|
||||||
|
:height="croppaHeight"
|
||||||
|
:disable-drag-to-move="false"
|
||||||
|
:disable-scroll-to-zoom="true"
|
||||||
|
:zoom-speed="1"
|
||||||
|
@loading-end="onCroppaImageLoaded()"
|
||||||
|
></croppa>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Controllers -->
|
||||||
|
<div
|
||||||
|
class="edit-page-controllers column is-3 is-flex-column is-justify-centered has-text-centered"
|
||||||
|
>
|
||||||
|
<div class="change-image">
|
||||||
|
<button class="button" @click="pages[currentPage].croppa.chooseFile()">
|
||||||
|
<i class="fa fa-fw fa-refresh"></i> Change Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="field" v-if="currentPage===0 && pages.length===1">
|
||||||
|
<label class="label">Page width</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="200"
|
||||||
|
max="600"
|
||||||
|
v-model="bookWidth"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
/>
|
||||||
|
<div class="is-flex is-justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="bookWidth-=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="bookWidth+=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field" v-if="currentPage===0 && pages.length===1">
|
||||||
|
<label class="label">Page height</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="200"
|
||||||
|
max="600"
|
||||||
|
v-model="bookHeight"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
/>
|
||||||
|
<div class="is-flex is-justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="bookHeight-=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="bookHeight+=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field"></div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Zoom Image</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1000"
|
||||||
|
v-model="pageZoom"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
/>
|
||||||
|
<div class="is-flex is-justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="pageZoom-=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button is-rounded is-outlined is-small"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
@click="pageZoom+=2"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rotations is-flex is-justify-between m-b-lg">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="onRotateClicked(false)"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-rotate-left"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
@click="onRotateClicked(true)"
|
||||||
|
:disabled="!pages[currentPage].imageLoaded"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-rotate-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="remove-image" v-if="currentPage === pages.length-1">
|
||||||
|
<button class="button is-danger" @click="deleteLastPage()">
|
||||||
|
<i class="fa fa-fw fa-trash"></i> Delete Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: Fix this shit - maybe fork the project? -->
|
||||||
|
<!-- <div class="field">
|
||||||
|
<label class="label">Rotate Image</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-180"
|
||||||
|
max="180"
|
||||||
|
v-model="pageRotation"
|
||||||
|
:disabled="!pages[currentPage].croppa.hasImage"
|
||||||
|
/>
|
||||||
|
</div>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<!-- Book Preview -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
:class="`is-fullheight-container ${flipbookRef ? '' : 'is-transparent'}`"
|
||||||
|
v-if="!editMode"
|
||||||
|
>
|
||||||
|
<div class="book-view m-sm m-r-md">
|
||||||
|
<div
|
||||||
|
class="go-left m-r-sm"
|
||||||
|
style="display: inline-block; align-items: center; position: absolute; left:0px; top:0px"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button book-flip-buttons"
|
||||||
|
:disabled="!canFlipLeft"
|
||||||
|
@click="onLeftClicked()"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-arrow-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<flipbook
|
||||||
|
class="flipbook"
|
||||||
|
:pages="previewPages"
|
||||||
|
:forwardDirection="book.ltr ? 'right': 'left'"
|
||||||
|
:zooms="null"
|
||||||
|
:enabled="true"
|
||||||
|
ref="flipbook"
|
||||||
|
v-slot="flipbook"
|
||||||
|
@on-mounted="bookMounted()"
|
||||||
|
>
|
||||||
|
<div class="page-progress has-text-centered m-b-none">
|
||||||
|
<p>Page {{ flipbook.page }} of {{ flipbook.numPages }}</p>
|
||||||
|
</div>
|
||||||
|
</flipbook>
|
||||||
|
<div
|
||||||
|
class="go-right m-l-sm"
|
||||||
|
style="display: inline-block; align-items: center; position: absolute; right:0px; top:0px"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button book-flip-buttons"
|
||||||
|
:disabled="!canFlipRight"
|
||||||
|
@click="onRightClicked()"
|
||||||
|
>
|
||||||
|
<i class="fa fa-fw fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { mapGetters, mapActions } from "vuex";
|
||||||
|
import Flipbook from "../components/flipbook/flipbook.cjs.js";
|
||||||
|
import Croppa from "vue-croppa";
|
||||||
|
import Services from "../../services";
|
||||||
|
export default {
|
||||||
|
name: "EditBook",
|
||||||
|
props: ["editBook"],
|
||||||
|
components: {
|
||||||
|
Flipbook,
|
||||||
|
Croppa: Croppa.component
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
editMode: function(newVal) {
|
||||||
|
if (!newVal) {
|
||||||
|
this.currentPage = -7;
|
||||||
|
//Update previewPages
|
||||||
|
this.previewPages = [null, ...this.pages.map(page => page.image)];
|
||||||
|
} else {
|
||||||
|
if (this.currentPage < 0 && this.pages.length) this.currentPage = 0;
|
||||||
|
this.flipbookRef = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentPage: async function(currentPage, lastPage) {
|
||||||
|
/// Save progress on latest page
|
||||||
|
console.log(lastPage, currentPage);
|
||||||
|
if (this.pages[lastPage] && lastPage >= 0) {
|
||||||
|
const imageBlob = await this.pages[lastPage].croppa.promisedBlob(
|
||||||
|
"image/jpg",
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
if (imageBlob) {
|
||||||
|
let url = URL.createObjectURL(imageBlob);
|
||||||
|
this.pages[lastPage].base64 = this.pages[
|
||||||
|
lastPage
|
||||||
|
].croppa.generateDataUrl("image/jpg", 0.8);
|
||||||
|
this.pages[lastPage].image = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentPage >= 0) {
|
||||||
|
// Load new page croppa
|
||||||
|
this.pages[currentPage].loaded = false;
|
||||||
|
this.$nextTick(function() {
|
||||||
|
console.log("tick");
|
||||||
|
this.pages[currentPage].loaded = true;
|
||||||
|
console.log(this.pages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.pageRotation = 0;
|
||||||
|
this.pageZoom = 500;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
pageRotation: function(newAngle) {
|
||||||
|
console.log(newAngle);
|
||||||
|
const page = this.pages[this.currentPage];
|
||||||
|
const canvas: HTMLCanvasElement = page.croppa.getCanvas();
|
||||||
|
const ctx: CanvasRenderingContext2D = page.croppa.getContext();
|
||||||
|
ctx.rotate((newAngle * Math.PI) / 180);
|
||||||
|
ctx.drawImage(canvas, 0, 0);
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
this.$nextTick(function() {
|
||||||
|
// page.croppa.moveDownwards(1);
|
||||||
|
});
|
||||||
|
// page.croppa.rotate(newAngle / 90);
|
||||||
|
},
|
||||||
|
pageZoom: function(newVal, oldVal) {
|
||||||
|
const page = this.pages[this.currentPage];
|
||||||
|
console.log(newVal, oldVal);
|
||||||
|
const delta = Math.abs(newVal - oldVal);
|
||||||
|
if (newVal < oldVal) {
|
||||||
|
//zoomOut
|
||||||
|
for (let i = delta; i > 0; i--) page.croppa.zoomOut();
|
||||||
|
} else {
|
||||||
|
//zoomIn
|
||||||
|
for (let i = delta; i > 0; i--) page.croppa.zoomIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.editBook) this.book = this.editBook;
|
||||||
|
else
|
||||||
|
this.book = {
|
||||||
|
name: "",
|
||||||
|
ltr: true,
|
||||||
|
pages: 0,
|
||||||
|
user_id: this.user.id
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(["notify", "getUser"]),
|
||||||
|
///
|
||||||
|
deleteLastPage() {
|
||||||
|
this.pages.pop();
|
||||||
|
|
||||||
|
if (!this.pages.length) this.currentPage = -7;
|
||||||
|
else this.currentPage = this.pages.length - 1;
|
||||||
|
},
|
||||||
|
onRotateClicked(clockwise: boolean) {
|
||||||
|
const page = this.pages[this.currentPage];
|
||||||
|
page.croppa.rotate(clockwise ? 1 : -1);
|
||||||
|
},
|
||||||
|
onPageClicked(pageIndex) {
|
||||||
|
this.currentPage = pageIndex;
|
||||||
|
this.editMode = true;
|
||||||
|
},
|
||||||
|
onCroppaImageLoaded() {
|
||||||
|
this.pages[this.currentPage].imageLoaded = true;
|
||||||
|
},
|
||||||
|
async onUploadClicked() {
|
||||||
|
//TODO: Better validations
|
||||||
|
if (!this.book.title || !this.book.title.length) {
|
||||||
|
this.notify({
|
||||||
|
message: "Book needs a title!",
|
||||||
|
level: "warning"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.book.author || !this.book.author.length) {
|
||||||
|
this.notify({
|
||||||
|
message: "Book sure has an author!",
|
||||||
|
level: "warning"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.pages.length < 4) {
|
||||||
|
this.notify({
|
||||||
|
message: "A book need to have at least 4 pages",
|
||||||
|
level: "warning"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploading = true;
|
||||||
|
const resp = await Services.ApiService.uploadBook({
|
||||||
|
title: this.book.title,
|
||||||
|
author: this.book.author,
|
||||||
|
ltr: this.book.ltr,
|
||||||
|
pages: this.pages.map(
|
||||||
|
p => p.base64 || p.croppa.generateDataUrl("image/jpg", 0.7)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if (resp.code === 0) {
|
||||||
|
this.notify({
|
||||||
|
message: `Woop Woop!! ${this.book.title} has been added!`,
|
||||||
|
level: "success"
|
||||||
|
});
|
||||||
|
this.getUser();
|
||||||
|
this.$router.replace({ path: `/` });
|
||||||
|
} else {
|
||||||
|
this.notify({
|
||||||
|
message: `Something went wrong!`,
|
||||||
|
level: "danger"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.uploading = false;
|
||||||
|
},
|
||||||
|
async onAddPageClicked() {
|
||||||
|
const lastPage = this.pages.length - 1;
|
||||||
|
if (lastPage < 0) {
|
||||||
|
this.pages.push({
|
||||||
|
text: "Cover",
|
||||||
|
id: 0,
|
||||||
|
loaded: false,
|
||||||
|
croppa: {},
|
||||||
|
image: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
base64: null
|
||||||
|
});
|
||||||
|
this.currentPage = 0;
|
||||||
|
//return;
|
||||||
|
} else {
|
||||||
|
console.log("In else");
|
||||||
|
const nextPage = lastPage + 1;
|
||||||
|
const imageBlob = await this.pages[lastPage].croppa.promisedBlob(
|
||||||
|
"image/jpg",
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
if (!imageBlob) {
|
||||||
|
this.notify({
|
||||||
|
message: "Your last page is still empty",
|
||||||
|
level: "warning"
|
||||||
|
});
|
||||||
|
this.currentPage = lastPage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let url = URL.createObjectURL(imageBlob);
|
||||||
|
this.pages.base64 = this.pages[lastPage].croppa.generateDataUrl(
|
||||||
|
"image/jpg",
|
||||||
|
0.8
|
||||||
|
);
|
||||||
|
this.pages[lastPage].image = url;
|
||||||
|
this.pages.push({
|
||||||
|
text: `Page ${nextPage}`,
|
||||||
|
id: nextPage,
|
||||||
|
loaded: false,
|
||||||
|
croppa: {},
|
||||||
|
image: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
base64: null
|
||||||
|
});
|
||||||
|
this.currentPage = nextPage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bookMounted() {
|
||||||
|
if (this.$refs.flipbook) {
|
||||||
|
console.log("Found!");
|
||||||
|
this.flipbookRef = true;
|
||||||
|
// this.$refs.flipbook.onResize();
|
||||||
|
// console.log("resized");
|
||||||
|
} else {
|
||||||
|
console.log("Still Null!!");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLeftClicked() {
|
||||||
|
console.time("Flip Left");
|
||||||
|
this.$refs.flipbook.flipLeft();
|
||||||
|
console.timeEnd("Flip Left");
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onRightClicked() {
|
||||||
|
this.$refs.flipbook.flipRight();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(["user"]),
|
||||||
|
//
|
||||||
|
bookPages() {
|
||||||
|
return this.pages.slice(1);
|
||||||
|
},
|
||||||
|
canFlipLeft() {
|
||||||
|
return this.flipbookRef && this.$refs.flipbook.canFlipLeft;
|
||||||
|
},
|
||||||
|
canFlipRight() {
|
||||||
|
return this.flipbookRef && this.$refs.flipbook.canFlipRight;
|
||||||
|
},
|
||||||
|
croppaWidth() {
|
||||||
|
return Number(this.bookWidth);
|
||||||
|
},
|
||||||
|
croppaHeight() {
|
||||||
|
return Number(this.bookHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
book: {
|
||||||
|
title: "",
|
||||||
|
author: "",
|
||||||
|
rtl: true
|
||||||
|
},
|
||||||
|
pages: [],
|
||||||
|
bookWidth: 400,
|
||||||
|
bookHeight: 600,
|
||||||
|
previewPages: [],
|
||||||
|
editMode: true,
|
||||||
|
currentPage: -7,
|
||||||
|
flipbookRef: false,
|
||||||
|
pageRotation: 0,
|
||||||
|
pageZoom: 500,
|
||||||
|
uploading: false,
|
||||||
|
errors: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -230,6 +230,9 @@ export default {
|
||||||
case "child":
|
case "child":
|
||||||
this.showAddChildModal = true;
|
this.showAddChildModal = true;
|
||||||
break;
|
break;
|
||||||
|
case "book":
|
||||||
|
this.$router.push({ path: `/create/${action}` });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.notify({
|
this.notify({
|
||||||
message: `Add ${action} button clicked. Still not working`
|
message: `Add ${action} button clicked. Still not working`
|
||||||
|
|
|
@ -19,6 +19,24 @@ export default class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async uploadBook(payload: { title: string, author: string, pages: string[], ltr: boolean }) {
|
||||||
|
// console.log(payload);
|
||||||
|
// return { code: 0 }
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (await fetch('/api/v1/client/book/create', options)).json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`uploadBook ERROR: ${e.message}`);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async updateUser(payload: { name?: string; avatar?: string; profile_cover?: string; email?: string }) {
|
static async updateUser(payload: { name?: string; avatar?: string; profile_cover?: string; email?: string }) {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|
|
@ -49,6 +49,7 @@ Route
|
||||||
Route.get('child/:id', 'ClientApiController.getChild');
|
Route.get('child/:id', 'ClientApiController.getChild');
|
||||||
Route.post('child/:id', 'ClientApiController.updateChild');
|
Route.post('child/:id', 'ClientApiController.updateChild');
|
||||||
Route.post('call/create', 'ClientApiController.createCall');
|
Route.post('call/create', 'ClientApiController.createCall');
|
||||||
|
Route.post('book/create', 'ClientApiController.createBook');
|
||||||
})
|
})
|
||||||
.prefix('api/v1/client')
|
.prefix('api/v1/client')
|
||||||
.middleware(['auth']);
|
.middleware(['auth']);
|
||||||
|
|
Reference in a new issue