644 lines
24 KiB
Vue
644 lines
24 KiB
Vue
<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="currentPage = -7;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 has-text-centered">
|
|
<div v-if="pages[currentPage].loaded">
|
|
<h1 class="subtitle is-3">{{pages[currentPage].text}}</h1>
|
|
<croppa
|
|
v-model="pages[currentPage].croppa"
|
|
:prevent-white-space="true"
|
|
:show-remove-button="false"
|
|
:accept="'image/*'"
|
|
: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="field" v-if="currentPage===0 && pages.length===1">
|
|
<label class="label">Page width</label>
|
|
<input
|
|
type="range"
|
|
:min="DEFAULT_PAGE_WIDTH-50"
|
|
:max="DEFAULT_PAGE_WIDTH"
|
|
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 || bookWidth<= DEFAULT_PAGE_WIDTH - 50"
|
|
@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 || bookWidth >= DEFAULT_PAGE_WIDTH"
|
|
@click="bookWidth+=2"
|
|
>
|
|
<i class="fa fa-fw fa-plus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="prev-page-preview" v-else>
|
|
<div
|
|
class="is-flex is-justify-centered"
|
|
v-if="pages[currentPage - 1] && pages[currentPage - 1].base64"
|
|
>
|
|
<div class="book-thumb page-preview">
|
|
<div class="book-text">
|
|
<div>Previouse Page</div>
|
|
</div>
|
|
<div class="book-cover">
|
|
<img
|
|
:src="pages[currentPage - 1] ? pages[currentPage - 1].base64: ''"
|
|
/>
|
|
</div>
|
|
<div class="book-text">
|
|
<div>{{pages[currentPage - 1].text}}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="is-fullwidth-container" />
|
|
<div class="field" v-if="currentPage===0 && pages.length===1">
|
|
<label class="label">Page height</label>
|
|
<input
|
|
type="range"
|
|
:min="DEFAULT_PAGE_HEIGHT-50"
|
|
:max="DEFAULT_PAGE_HEIGHT"
|
|
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 || bookWidth<= DEFAULT_PAGE_HEIGHT - 50"
|
|
@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 || bookWidth >= DEFAULT_PAGE_HEIGHT"
|
|
@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="change-image">
|
|
<button
|
|
class="button is-fullwidth-container"
|
|
@click="pages[currentPage].croppa.chooseFile()"
|
|
>
|
|
<i class="fa fa-fw fa-refresh"></i> Change Image
|
|
</button>
|
|
</div>
|
|
<div class="remove-image" v-if="currentPage === pages.length-1">
|
|
<button
|
|
class="button is-danger is-fullwidth-container"
|
|
@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";
|
|
|
|
const DEFAULT_PAGE_WIDTH = 350;
|
|
const DEFAULT_PAGE_HEIGHT = 350;
|
|
|
|
const DEFAULT_ZOOM = 500;
|
|
const MIME_TYPE = "image/jpeg";
|
|
const COMPRESSION_RATE = 0.4;
|
|
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(
|
|
MIME_TYPE,
|
|
COMPRESSION_RATE
|
|
);
|
|
if (imageBlob) {
|
|
let url = URL.createObjectURL(imageBlob);
|
|
this.pages[lastPage].base64 = this.pages[
|
|
lastPage
|
|
].croppa.generateDataUrl(MIME_TYPE, COMPRESSION_RATE);
|
|
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 = DEFAULT_ZOOM;
|
|
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 promiseAllProgress(
|
|
promises: Promise<any>[],
|
|
callback: (done: number, total: number) => void
|
|
): Promise<any> {
|
|
let counter = 0;
|
|
callback(counter, promises.length);
|
|
for (let i = 0; i < promises.length; i++) {
|
|
const promise = promises[i];
|
|
promise.then(async val => {
|
|
counter++;
|
|
callback(counter, promises.length);
|
|
return val;
|
|
});
|
|
}
|
|
return Promise.all(promises);
|
|
},
|
|
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;
|
|
try {
|
|
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(MIME_TYPE, COMPRESSION_RATE)
|
|
)
|
|
});
|
|
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"
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.log(`Error... ${e.message}`);
|
|
}
|
|
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(
|
|
MIME_TYPE,
|
|
COMPRESSION_RATE
|
|
);
|
|
if (!imageBlob) {
|
|
this.notify({
|
|
message: "Your last page is still empty",
|
|
level: "warning"
|
|
});
|
|
this.currentPage = lastPage;
|
|
return;
|
|
}
|
|
let url = URL.createObjectURL(imageBlob);
|
|
this.pages[lastPage].base64 = this.pages[
|
|
lastPage
|
|
].croppa.generateDataUrl(MIME_TYPE, COMPRESSION_RATE);
|
|
const c01 = this.pages[lastPage].croppa.generateDataUrl(MIME_TYPE, 0.1);
|
|
console.log(`COMPRESSION_RATE: ${this.pages[lastPage].base64.length}`);
|
|
console.log(`0.1: ${c01.length}`);
|
|
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: DEFAULT_PAGE_WIDTH,
|
|
bookHeight: DEFAULT_PAGE_HEIGHT,
|
|
previewPages: [],
|
|
editMode: true,
|
|
currentPage: -7,
|
|
flipbookRef: false,
|
|
pageRotation: 0,
|
|
pageZoom: DEFAULT_ZOOM,
|
|
uploading: false,
|
|
errors: {},
|
|
DEFAULT_PAGE_WIDTH,
|
|
DEFAULT_PAGE_HEIGHT
|
|
};
|
|
}
|
|
};
|
|
</script>
|