First implementation of a book uploader

This commit is contained in:
Sagi Dayan 2020-05-24 15:59:44 -04:00
parent e2a35571cf
commit f1cba399af
14 changed files with 8220 additions and 335 deletions

View file

@ -236,6 +236,31 @@ class ClientApiController {
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

View file

@ -2,11 +2,12 @@ const Drive = use('Drive');
class FileUtils {
static async saveBase64File(base64Str) {
static async saveBase64File(base64Str, _fileName = null) {
console.log(base64Str.length);
const parsed = parseBase64(base64Str);
const fileName =
const fileName = _fileName ||
`${Date.now()}-${Math.random() * 1000}.${parsed.extension}`;
console.log(fileName);
const file = await Drive.put(fileName, parsed.data);
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

View file

@ -7886,6 +7886,12 @@ html {
.is-fullheight {
min-height: calc(100vh - ( 3.25rem )); }
.is-fullwidth-container {
width: 100%; }
.is-fullheight-container {
height: 100%; }
.is-fullwidth {
width: 100vw;
overflow: hidden; }
@ -8003,11 +8009,18 @@ video {
height: 100%; }
.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 {
opacity: 0; }
.fade-enter-to, .fade-leave {
opacity: 1; }
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
@ -8017,6 +8030,10 @@ video {
transition-timing-function: ease-in-out;
overflow: hidden; }
.slide-left-enter-active,
.slide-right-enter-active {
transition-delay: 0.2s; }
.slide-left-enter,
.slide-right-leave-active {
opacity: 0;
@ -8036,6 +8053,9 @@ video {
.is-justify-centered {
justify-content: center; }
.is-justify-between {
justify-content: space-between; }
.video-side-bar {
height: calc(100vh - ( 3.25rem )); }
.video-side-bar video {
@ -8085,3 +8105,34 @@ video {
display: flex;
flex-flow: column;
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; }

View file

@ -221,6 +221,12 @@ $sizes: (
// overflow-y: auto;
// }
}
.is-fullwidth-container{
width:100%;
}
.is-fullheight-container{
height:100%;
}
.is-fullwidth{
width: 100vw;
@ -368,11 +374,19 @@ video{
}
//Fade vue transition
.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;
}
.fade-enter-to, .fade-leave{
opacity: 1;
}
.slide-left-enter-active,
.slide-left-leave-active,
@ -383,6 +397,10 @@ video{
transition-timing-function: ease-in-out;
overflow: hidden;
}
.slide-left-enter-active,
.slide-right-enter-active{
transition-delay: 0.2s;
}
.slide-left-enter,
.slide-right-leave-active {
@ -405,6 +423,10 @@ video{
.is-justify-centered{
justify-content: center;
}
.is-justify-between{
justify-content: space-between;
}
.video-side-bar{
height: calc(100vh - ( #{$navbar-height} ) );
@ -465,3 +487,41 @@ video{
flex-flow: column;
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;
}
}

View file

@ -14,6 +14,7 @@ import Home from "../views/home.vue";
import Settings from "../views/settings.vue";
import Call from "../views/call.vue";
import ChildProfile from "../views/child_profile.vue";
import EditBook from "../views/edit_book.vue";
// Call Views
import CallLobby from "../views/call_views/Lobby.vue";
@ -30,6 +31,10 @@ const routes: RouteConfig[] = [
path: "/settings",
component: Settings
},
{
path: "/create/book",
component: EditBook
},
{
path: "/call/:id",
component: Call,

View file

@ -1,7 +1,7 @@
<template>
<div>
<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="go-left m-r-sm"

View 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>

View file

@ -230,6 +230,9 @@ export default {
case "child":
this.showAddChildModal = true;
break;
case "book":
this.$router.push({ path: `/create/${action}` });
break;
default:
this.notify({
message: `Add ${action} button clicked. Still not working`

View file

@ -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 }) {
const options = {
method: 'PUT',

View file

@ -49,6 +49,7 @@ Route
Route.get('child/:id', 'ClientApiController.getChild');
Route.post('child/:id', 'ClientApiController.updateChild');
Route.post('call/create', 'ClientApiController.createCall');
Route.post('book/create', 'ClientApiController.createBook');
})
.prefix('api/v1/client')
.middleware(['auth']);