Compare commits
10 commits
e2a35571cf
...
3364adbe31
Author | SHA1 | Date | |
---|---|---|---|
Sagi Dayan | 3364adbe31 | ||
Sagi Dayan | c75afd74f9 | ||
Sagi Dayan | 0e256b5fab | ||
Sagi Dayan | c9239a3aa5 | ||
Sagi Dayan | 74382d9d17 | ||
Sagi Dayan | d81379d425 | ||
Sagi Dayan | f81038c171 | ||
Sagi Dayan | 2bf05c4690 | ||
Sagi Dayan | 31f01a2850 | ||
Sagi Dayan | f1cba399af |
|
@ -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
|
||||
|
|
33
app/Middleware/BookCallPageAuth.js
Normal file
33
app/Middleware/BookCallPageAuth.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
'use strict'
|
||||
/** @typedef {import('@adonisjs/framework/src/Request')} Request */
|
||||
/** @typedef {import('@adonisjs/framework/src/Response')} Response */
|
||||
/** @typedef {import('@adonisjs/framework/src/View')} View */
|
||||
|
||||
class BookCallPageAuth {
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {Request} ctx.request
|
||||
* @param {Function} next
|
||||
*/
|
||||
async handle(ctx, next) {
|
||||
const {request, auth, response, book, call} = ctx;
|
||||
// call next to advance the request
|
||||
const user = auth.user;
|
||||
if (book.user_id) {
|
||||
// Belongs to a user. Check if the book user has a connection with this
|
||||
// user
|
||||
if (book.user_id === user.id) {
|
||||
await next();
|
||||
} else if (call.parent_id === user.id || call.guest_id === user.id) {
|
||||
await next();
|
||||
} else {
|
||||
response.status(403);
|
||||
response.send({code: 403, message: 'Book is private'});
|
||||
}
|
||||
} else {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BookCallPageAuth
|
|
@ -2,8 +2,7 @@
|
|||
/** @typedef {import('@adonisjs/framework/src/Request')} Request */
|
||||
/** @typedef {import('@adonisjs/framework/src/Response')} Response */
|
||||
/** @typedef {import('@adonisjs/framework/src/View')} View */
|
||||
const Book = use('App/Models/Book');
|
||||
const UserChildUtils = use('App/Utils/UserChildUtils');
|
||||
|
||||
class BookPageAuth {
|
||||
/**
|
||||
* @param {object} ctx
|
||||
|
@ -11,7 +10,7 @@ class BookPageAuth {
|
|||
* @param {Function} next
|
||||
*/
|
||||
async handle(ctx, next) {
|
||||
const {request, auth, response, book, call} = ctx;
|
||||
const {request, auth, response, book} = ctx;
|
||||
// call next to advance the request
|
||||
const user = auth.user;
|
||||
if (book.user_id) {
|
||||
|
@ -19,8 +18,6 @@ class BookPageAuth {
|
|||
// user
|
||||
if (book.user_id === user.id) {
|
||||
await next();
|
||||
} else if (call.parent_id === user.id || call.guest_id === user.id) {
|
||||
await next();
|
||||
} else {
|
||||
response.status(403);
|
||||
response.send({code: 403, message: 'Book is private'});
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -1,38 +1,81 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="416.217px" height="320.271px" viewBox="0 0 416.217 320.271" enable-background="new 0 0 416.217 320.271"
|
||||
xml:space="preserve">
|
||||
<path fill="none" d="M394.117,52.481c-7.972-2.006-13.092,8.789-6.313,12.514c2.938,1.615,16.261-1.004,19.756-0.984
|
||||
c13.258,0.076,29.066-2.875,42.151-0.119c1.609-4.211,1.439-8.369-1.647-11.504c-14.189,6.227-40.318,0.645-54.844,0.094"/>
|
||||
<path fill="#1D1B88" d="M37.057,127.323c19.387-9.344,40.18-9.455,61.592-1.916c13.055,4.596,23.707,13.828,36.516,18.154
|
||||
c14.568,4.918,28.163,9.368,43.853,10.801c33.112,3.021,64.396,11.681,98.155,8.614c31.721-2.879,52.089-24.647,53.298-58.286
|
||||
c0.594-16.504-8.058-31.07-6.314-48.687c1.783-18.01,7.156-25.946,19.578-37.653c5.897,8.243,10.637,17.251,14.088,26.606
|
||||
c3.865,10.486-0.469,17.875,0.789,29.094c6.428-28.464,21.398-30.406,45.775-36.254c2.444,13.24-0.197,34.666-6.396,46.003
|
||||
c-7.09,12.964-17.443,21.88-29.76,28.632c-16.209,8.888-25.014,13.044-33.404,30.286c-11.938,24.54-25.392,49.821-45.306,68.679
|
||||
c-18.49,17.508-52.829,32.369-77.049,34.816c-32.284,3.262-64.088,7.459-93.959-5.669c-25.17-11.062-44.333-27.558-61.878-49.198
|
||||
c-4.621-5.701-6.869-12.688-11.482-18.369c-6.27-7.721-11.066-5.008-18.303-9.153c-12.646-7.247,1.549-28.844,10.208-35.418"/>
|
||||
<path fill="#EAB0D4" d="M202.008,159.699c-0.453,3.444-9.512,17.323-13.782,19.231c-9.204,4.111-19.131-5.356-28.859-2.859
|
||||
c-0.416,6.123,11.912,9.218,16.833,12.529c8.162,5.492,9.108,6.719,18.266,3.275c8.433-3.171,14.195-7.451,23.204-7.35
|
||||
c8.942,0.103,15.044,0.115,22.972-1.111c15.088-2.335,38.19-8.296,52.227-2.977c13.217,5.012,21.434,16.401,36.277,19.481
|
||||
c15.463,3.207,31.297,10.226,47.059,10.402c0.525-1.664,0.492-3.628,0.285-5.08c-6.301-1.664-11.668-3.777-17.117-7.433
|
||||
c-1.94-1.302-1.83-5.287-4.885-6.824c-1.937-0.975-7.66,0.775-10.437,0.045c-10.049-2.648-28.935-15.67-30.334-25.93
|
||||
c-1.363-10.001,8.255-30.093,11.693-39.742c5.719-16.053,6.162-24.084-0.387-40.031c-6.857,0.03-6.359,21.592-8.203,27.875
|
||||
c-2.6,8.861-5.125,19.344-12.271,25.528c-19.594-12.017-35.82,7.034-52.429,5.838c-12.344-0.89-13.365,1.892-23.172,8.433
|
||||
c-7.848,5.234-18.899,6.94-26.94,9.938"/>
|
||||
<path fill="#EDECDC" d="M21.397,158.338c0,36.892-5.496,68.336,21.169,97.22c9.782,10.596,20.312,23.77,31.315,33.293
|
||||
c12.66,10.959,28.458,12.062,43.483,17.739c28.911,10.922,71.75,5.123,100.045-5.882c16.802-6.533,27.143-10.104,39.146-24.611
|
||||
c9.9-11.968,22.979-18.481,31.287-32.391c5.867-9.82,22.979-37.454,18.613-48.248c-11.146,8.85-18.016,20.773-31.138,28.797
|
||||
c-15.161,9.27-29.567,18.252-47.212,21.878c-33.11,6.804-67.33,6.304-99.181-3.477c-11.869-3.646-24.959-4.321-35.498-12.154
|
||||
c-9.404-6.987-18.122-15.58-25.811-24.883c-8.288-10.027-11.969-23.651-19.105-32.127c-5.23-6.212-26.525-9.255-25.028-15.154"/>
|
||||
<path fill="#FFEEF2" d="M146.676,164.821c10.386,9.195,24.997,14.287,38.366,17.131c7.076-6.071,27.167-21.166,4.409-22.615
|
||||
c-2.687-0.17-6.324,3.033-9.37,3.319c-4.036,0.379-7.785-1.337-11.224-1.343c-5.205-0.008-16.47-0.447-20.743,3.772
|
||||
c0.902,1.367,1.76,2.034,2.739,2.976"/>
|
||||
<path fill="#F4F0A2" d="M157.116,126.721c-1.388-3.18-13.381-0.909-17.772,4.326c-2.879,3.43-6.806,19.18-6.312,22.638
|
||||
c1.107,7.775,9.504,13.328,16.778,13.019c3.578-0.152,7.02-4.014,9.604-4.107c5.088-0.185,5.103,3.532,9.42,3.859
|
||||
c7.792,0.59,11.137-6.646,18.586-4.342c8.555,2.645,7.493,8.214,17.95,4.826c4.648-1.506,16.479-5.07,19.89-8.589
|
||||
c8.562-8.832-1.181-11.038-3.178-18.907c-2.157-8.493,4.019-12.391-4.18-19.447c-5.864-5.047-15.099-7.182-22.385-6.511
|
||||
c-13.268,1.222-26.209,11.132-38.399,14.315"/>
|
||||
<ellipse fill="#E6DDE1" cx="96.131" cy="179.692" rx="3.597" ry="3.292"/>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="whale.svg"
|
||||
xml:space="preserve"
|
||||
enable-background="new 0 0 416.217 320.271"
|
||||
viewBox="0 0 416.217 320.271"
|
||||
height="320.271px"
|
||||
width="416.217px"
|
||||
y="0px"
|
||||
x="0px"
|
||||
id="Layer_1"
|
||||
version="1.1"><metadata
|
||||
id="metadata25"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs23" /><sodipodi:namedview
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="160.1355"
|
||||
inkscape:cx="208.10851"
|
||||
inkscape:zoom="2.8663226"
|
||||
showgrid="false"
|
||||
id="namedview21"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<path
|
||||
id="path2"
|
||||
d="M12.986,129.484c-2.773,36.785-10.619,67.726,13.799,98.534c8.958,11.302,18.468,25.23,28.724,35.555 c11.8,11.88,27.47,14.167,42.025,20.957c28.008,13.065,71.161,10.505,100.205,1.657c17.246-5.25,27.826-8.032,40.886-21.597 c10.771-11.19,24.304-16.7,33.633-29.946c6.59-9.352,25.73-35.62,22.189-46.713c-11.781,7.987-19.527,19.36-33.217,26.374 c-15.814,8.104-30.854,15.978-48.723,18.267c-33.527,4.295-67.614,1.224-98.639-10.925c-11.561-4.527-24.563-6.187-34.483-14.789 c-8.853-7.675-16.899-16.899-23.867-26.754c-7.51-10.62-10.157-24.481-16.635-33.471c-4.749-6.588-25.754-11.223-23.818-16.992"
|
||||
fill="#F7F4E2" />
|
||||
<path
|
||||
id="path4"
|
||||
d="M394.737,67.927c-7.628-3.066-14.161,6.937-7.948,11.544c2.692,1.998,16.247,1.206,19.707,1.699 c13.126,1.868,29.188,1.084,41.78,5.585c2.163-3.955,2.558-8.097-0.076-11.621c-14.9,4.25-40.034-4.817-54.352-7.328"
|
||||
fill="none" />
|
||||
<ellipse
|
||||
id="ellipse6"
|
||||
ry="3.597"
|
||||
rx="3.292"
|
||||
cy="153.644"
|
||||
cx="82.278"
|
||||
fill="#FCFCFC"
|
||||
transform="matrix(0.1353 -0.9908 0.9908 0.1353 -81.0843 214.3818)" />
|
||||
|
||||
<path
|
||||
id="path10"
|
||||
d="M29.667,101.408c20.163-7.524,40.878-5.726,61.508,3.748c12.578,5.775,22.337,15.946,34.695,21.43 c14.054,6.235,27.184,11.915,42.675,14.781c32.696,6.048,63.052,17.544,96.95,17.591c31.852,0.045,54.133-19.761,58.424-53.146 c2.107-16.38-5.17-31.679-1.816-49.062c3.429-17.77,9.508-25.179,22.952-35.696c5.116,8.75,9.009,18.155,11.587,27.787 c2.885,10.797-2.108,17.756-1.887,29.043c9.016-27.753,24.1-28.312,48.911-31.897c1.217,13.409-3.38,34.501-10.594,45.221 c-8.25,12.259-19.378,20.187-32.263,25.779c-16.957,7.362-26.106,10.692-36.044,27.09c-14.141,23.341-29.859,47.279-51.421,64.229 c-20.019,15.737-55.577,27.382-79.919,27.594c-32.447,0.285-64.502,1.544-93.042-14.271c-24.048-13.325-41.616-31.512-57.1-54.671 c-4.078-6.102-5.675-13.266-9.747-19.346c-5.534-8.265-10.559-6.002-17.385-10.795c-11.927-8.378,4.19-28.58,13.417-34.331"
|
||||
fill="#F7DFF3" />
|
||||
<path
|
||||
id="path12"
|
||||
d="M185.766,139.689c-1.369,3.194-13.844,14.104-18.472,14.786c-9.973,1.467-16.968-10.333-27.009-10.561 c-2.057,5.782,8.974,12.097,12.816,16.617c6.371,7.496,6.95,8.932,16.698,8.094c8.976-0.772,15.682-3.333,24.327-0.799 c8.581,2.519,14.453,4.182,22.418,5.146c15.157,1.835,39.012,2.346,51.086,11.265c11.368,8.399,16.196,21.587,29.653,28.568 c14.019,7.271,27.363,18.312,42.49,22.745c0.955-1.461,1.454-3.359,1.649-4.813c-5.617-3.307-10.212-6.793-14.469-11.786 c-1.516-1.779-0.332-5.585-2.857-7.892c-1.601-1.463-7.582-1.327-10.059-2.781c-8.958-5.268-23.617-22.911-22.188-33.168 c1.393-9.997,16.089-26.739,22.009-35.097c9.849-13.907,12.447-21.519,10.457-38.644c-6.609-1.825-11.963,19.067-15.438,24.617 c-4.899,7.828-10.167,17.236-18.72,21.256c-15.611-16.87-36.387-2.918-52.052-8.563c-11.644-4.197-13.379-1.795-24.591,1.849 c-8.974,2.916-20.074,1.569-28.625,2.279"
|
||||
fill="#DFEAEA" />
|
||||
<path
|
||||
id="path14"
|
||||
d="M129.111,128.649c7.511,11.663,20.2,20.518,32.301,26.873c8.455-3.931,31.88-13.027,10.363-20.58 c-2.542-0.891-6.909,1.209-9.918,0.661c-3.988-0.727-7.133-3.393-10.443-4.33c-5.008-1.416-15.734-4.886-20.99-1.98 c0.499,1.56,1.145,2.434,1.832,3.606"
|
||||
fill="#F2E9D7" />
|
||||
<path
|
||||
id="path16"
|
||||
d="M152.468,94.796c-0.477-3.437-12.637-4.495-18.28-0.644c-3.699,2.524-11.741,16.624-12.201,20.086 c-1.038,7.784,5.543,15.402,12.629,17.072c3.486,0.822,7.845-1.964,10.357-1.355c4.948,1.198,3.957,4.78,8.025,6.263 c7.341,2.677,12.519-3.384,19.067,0.849c7.521,4.86,4.991,9.935,15.975,9.502c4.881-0.192,17.235-0.423,21.472-2.888 c10.632-6.187,1.85-10.946,2.056-19.062c0.222-8.76,7.221-10.841,1.237-19.853c-4.28-6.445-12.593-10.999-19.789-12.324 c-13.104-2.414-28.244,3.626-40.84,3.393"
|
||||
fill="#E0E099" />
|
||||
<path
|
||||
id="path18"
|
||||
d="M82.609,147.635c-6.449,0-6.449,10,0,10S89.057,147.635,82.609,147.635z"
|
||||
fill="#F9F9F9" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.1 KiB |
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
101
public/style.css
101
public/style.css
|
@ -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; }
|
||||
|
@ -7978,11 +7984,18 @@ video {
|
|||
border: solid 1px rgba(56, 181, 187, 0.3);
|
||||
flex-basis: 100%; }
|
||||
|
||||
.has-wrap {
|
||||
flex-wrap: wrap; }
|
||||
|
||||
.book-thumb {
|
||||
transition: all .2s;
|
||||
z-index: inherit;
|
||||
flex-basis: 12%;
|
||||
text-align: center; }
|
||||
text-align: center;
|
||||
min-width: 180px;
|
||||
background-color: whitesmoke;
|
||||
padding: 5px;
|
||||
margin: 5px; }
|
||||
|
||||
.book-thumb.enabled {
|
||||
cursor: pointer; }
|
||||
|
@ -8003,11 +8016,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 +8037,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 +8060,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 +8112,73 @@ video {
|
|||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between; }
|
||||
|
||||
.page-editor .croppa-container {
|
||||
border: 2px solid whitesmoke;
|
||||
border-radius: 8px;
|
||||
color: white; }
|
||||
|
||||
.book-stitch-preview-left::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
left: -50%;
|
||||
top: 0;
|
||||
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.3) 0%, transparent 100%);
|
||||
background: url("/images/default-user-avatar.png"); }
|
||||
|
||||
.stitch-preview-right {
|
||||
border: solid 1px whitesmoke; }
|
||||
.stitch-preview-right::after {
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: linear-gradient(to right, transparent 0%, white 80%); }
|
||||
|
||||
.stitch-preview-left {
|
||||
border: solid 1px whitesmoke; }
|
||||
.stitch-preview-left::after {
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: linear-gradient(to left, transparent 0%, white 80%); }
|
||||
|
||||
.book-stitch-preview-right::after {
|
||||
content: '';
|
||||
background: url("/images/default-user-avatar.png"); }
|
||||
|
||||
.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; }
|
||||
|
||||
.height-max-view {
|
||||
max-height: calc(99vh - ( 3.25rem )); }
|
||||
|
||||
.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;
|
||||
// }
|
||||
}
|
||||
.is-fullwidth-container{
|
||||
width:100%;
|
||||
}
|
||||
.is-fullheight-container{
|
||||
height:100%;
|
||||
}
|
||||
|
||||
.is-fullwidth{
|
||||
width: 100vw;
|
||||
|
@ -336,6 +342,9 @@ video{
|
|||
flex-basis: 100%;
|
||||
// max-width: 15vh;
|
||||
}
|
||||
.has-wrap{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.book-thumb{
|
||||
// cursor: not-allowed;
|
||||
|
@ -343,6 +352,10 @@ video{
|
|||
z-index: inherit;
|
||||
flex-basis: 12%;
|
||||
text-align: center;
|
||||
min-width: 180px;
|
||||
background-color: whitesmoke;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
.book-thumb.enabled{
|
||||
cursor: pointer;
|
||||
|
@ -368,11 +381,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 +404,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 +430,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 +494,100 @@ video{
|
|||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
//edit page
|
||||
.page-editor{
|
||||
|
||||
.croppa-container {
|
||||
border: 2px solid whitesmoke;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.book-thumb.page-preview{
|
||||
// flex-basis: unset;
|
||||
}
|
||||
.book-stitch-preview-left{
|
||||
&::before{
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
// background-color: black;
|
||||
width:50%;
|
||||
left:-50%;
|
||||
top:0;
|
||||
mask-image: linear-gradient(to left, rgba(0,0,0,.3) 0%, transparent 100%);
|
||||
background: url('/images/default-user-avatar.png');
|
||||
}
|
||||
}
|
||||
.stitch-preview-right{
|
||||
// position: absolute;
|
||||
// top:0;
|
||||
// right:0;
|
||||
border: solid 1px whitesmoke;
|
||||
&::after{
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
// background-color: black;
|
||||
width:100%;
|
||||
left:0;
|
||||
top:0;
|
||||
background: linear-gradient(to right, transparent 0%, white 80%);
|
||||
}
|
||||
}
|
||||
.stitch-preview-left{
|
||||
// position: absolute;
|
||||
// top:0;
|
||||
// left:0;
|
||||
border: solid 1px whitesmoke;
|
||||
&::after{
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
// background-color: black;
|
||||
width:100%;
|
||||
left:0;
|
||||
top:0;
|
||||
background: linear-gradient(to left, transparent 0%, white 80%);
|
||||
}
|
||||
|
||||
}
|
||||
.book-stitch-preview-right{
|
||||
&::after{
|
||||
content: '';
|
||||
|
||||
background: url('/images/default-user-avatar.png');
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.height-max-view{
|
||||
max-height: calc(99vh - ( #{$navbar-height} ) );
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:prevent-white-space="true"
|
||||
:show-remove-button="false"
|
||||
:disable-drag-to-move="isDefaultImage"
|
||||
:accept="'image/*'"
|
||||
@new-image="isDefaultImage=false;zoomState=1"
|
||||
></croppa>
|
||||
</figure>
|
||||
|
|
|
@ -14,6 +14,8 @@ 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";
|
||||
import BookOfflineViewer from "../views/BookOfflineViewer.vue";
|
||||
|
||||
// Call Views
|
||||
import CallLobby from "../views/call_views/Lobby.vue";
|
||||
|
@ -30,6 +32,14 @@ const routes: RouteConfig[] = [
|
|||
path: "/settings",
|
||||
component: Settings
|
||||
},
|
||||
{
|
||||
path: "/create/book",
|
||||
component: EditBook
|
||||
},
|
||||
{
|
||||
path: "/book/:id",
|
||||
component: BookOfflineViewer
|
||||
},
|
||||
{
|
||||
path: "/call/:id",
|
||||
component: Call,
|
||||
|
|
137
resources/scripts/applications/home/views/BookOfflineViewer.vue
Normal file
137
resources/scripts/applications/home/views/BookOfflineViewer.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<div class="is-fullwidth is-fullheight-container p-l-lg p-r-lg">
|
||||
<Loading v-if="loading" />
|
||||
<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"
|
||||
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="pages"
|
||||
:forwardDirection="book.ltr ? 'right': 'left'"
|
||||
:zooms="null"
|
||||
:enabled="true"
|
||||
@on-mounted="bookMounted()"
|
||||
ref="flipbook"
|
||||
v-slot="flipbook"
|
||||
>
|
||||
<!-- @flip-left-start="onFlip('left')" -->
|
||||
<!-- @flip-right-start="onFlip('right')" -->
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import Flipbook from "../components/flipbook/flipbook.cjs.js";
|
||||
import Loading from "../../shared/components/Loading/Loading.vue";
|
||||
|
||||
export default {
|
||||
name: "BookOfflineViewer",
|
||||
components: {
|
||||
Flipbook,
|
||||
Loading
|
||||
},
|
||||
created() {
|
||||
const bookId = Number(this.$route.params.id);
|
||||
if (!this.user || !bookId) {
|
||||
this.$router.replace({ path: `/` });
|
||||
}
|
||||
|
||||
this.user.books.forEach(b => {
|
||||
if (this.book) return;
|
||||
if (b.id === bookId) {
|
||||
console.log("Found Book");
|
||||
this.book = b;
|
||||
}
|
||||
});
|
||||
if (!this.book) {
|
||||
this.notify({ message: "Book Not Found!", level: "danger" });
|
||||
this.$router.replace({ path: `/` });
|
||||
} else {
|
||||
// create pages
|
||||
// /u/books/:bookId/page/:pageNumber
|
||||
const pages = [null];
|
||||
for (let i = 1; i < this.book.pages + 1; i++) {
|
||||
pages.push(`/u/books/${bookId}/page/${i}`);
|
||||
}
|
||||
this.pages = pages;
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
bookMounted() {
|
||||
console.log("Book Mounted!");
|
||||
if (this.$refs.flipbook) {
|
||||
console.log("Found!");
|
||||
this.flipbookRef = true;
|
||||
// this.$refs.flipbook.onResize();
|
||||
// console.log("resized");
|
||||
} else {
|
||||
console.log("Still Null!!");
|
||||
}
|
||||
},
|
||||
onLeftClicked() {
|
||||
this.$refs.flipbook.flipLeft();
|
||||
},
|
||||
onRightClicked() {
|
||||
this.$refs.flipbook.flipRight();
|
||||
},
|
||||
...mapActions(["notify"])
|
||||
},
|
||||
computed: {
|
||||
canFlipLeft() {
|
||||
return this.flipbookRef && this.$refs.flipbook.canFlipLeft;
|
||||
},
|
||||
canFlipRight() {
|
||||
return this.flipbookRef && this.$refs.flipbook.canFlipRight;
|
||||
},
|
||||
...mapGetters(["user"])
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
book: <IBook>null,
|
||||
pages: <string[]>[],
|
||||
flipbookRef: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface IBook {
|
||||
id: number;
|
||||
pages: number;
|
||||
user_id?: number;
|
||||
title: string;
|
||||
ltr: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
</script>
|
|
@ -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"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h2 class="subtitle">
|
||||
<i class="fa fa-fw fa-book"></i> Select A Book
|
||||
</h2>
|
||||
<div class="is-flex m-b-md">
|
||||
<div class="is-flex m-b-md has-wrap">
|
||||
<div
|
||||
:class="['book-thumb', 'm-l-md', {'enabled': callManager.isHost}]"
|
||||
v-for="(book, index) in callManager.books"
|
||||
|
|
725
resources/scripts/applications/home/views/edit_book.vue
Normal file
725
resources/scripts/applications/home/views/edit_book.vue
Normal file
|
@ -0,0 +1,725 @@
|
|||
<template>
|
||||
<div class="is-fullwidth is-fullheight-container p-l-lg p-r-lg">
|
||||
<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 height-max-view">
|
||||
<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="onEditClicked()">
|
||||
<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>
|
||||
<div class="has-text-left">
|
||||
<p>Here will be some more instructions in the future...</p>
|
||||
<p>Also maybe some gifs fo help...</p>
|
||||
<ul>
|
||||
<li>Add a title and author for the book</li>
|
||||
<li>Add first image (Cover of the book)</li>
|
||||
<li>Try to fit the image as close as possible to edges of the "page"</li>
|
||||
<li>You can modify the width/height while editing the cover</li>
|
||||
<li>You can always zoom in/out and drag the image to make it fit better</li>
|
||||
<li>Please click on preview to actualy see how the book will look like before uploading</li>
|
||||
<li>Once all done. Click on "Upload Book"</li>
|
||||
</ul>
|
||||
</div>
|
||||
<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>
|
||||
<div class="is-relative is-flex is-justify-centered">
|
||||
<div
|
||||
class="stitch-preview-left is-relative"
|
||||
v-if="currentPage != 0 && currentPage % 2 === 0 && book.ltr"
|
||||
:style="`width:${croppaWidth}px;height:${croppaHeight}px;`"
|
||||
>
|
||||
<img :src="pages[currentPage-1].image" alt />
|
||||
</div>
|
||||
<croppa
|
||||
v-model="pages[currentPage].croppa"
|
||||
:prevent-white-space="false"
|
||||
: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"
|
||||
canvas-color="white"
|
||||
@loading-end="onCroppaImageLoaded()"
|
||||
></croppa>
|
||||
<div
|
||||
class="stitch-preview-right is-relative"
|
||||
v-if="currentPage != 0 && currentPage % 2 === 0 && !book.ltr"
|
||||
:style="`width:${croppaWidth}px;height:${croppaHeight}px;`"
|
||||
>
|
||||
<img :src="pages[currentPage-1].image" alt />
|
||||
</div>
|
||||
</div>
|
||||
</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-100"
|
||||
: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 - 100"
|
||||
@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 is-flex">
|
||||
<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="DEFAULT_ZOOM*2"
|
||||
v-model="pageZoom"
|
||||
:disabled="!pages[currentPage].imageLoaded"
|
||||
ref="zoomRangeSlider"
|
||||
@mouseup="blurZoomSlider()"
|
||||
/>
|
||||
<div class="is-flex is-justify-between">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-rounded is-outlined is-small"
|
||||
:disabled="!pages[currentPage].imageLoaded || pageZoom <= 0"
|
||||
@click="zoom(false)"
|
||||
>
|
||||
<i class="fa fa-fw fa-minus"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-rounded is-outlined is-small"
|
||||
:disabled="!pages[currentPage].imageLoaded || pageZoom >= DEFAULT_ZOOM * 2"
|
||||
@click="zoom(true)"
|
||||
>
|
||||
<i class="fa fa-fw fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rotations is-flex is-justify-between m-b-lg">
|
||||
<label class="label">Rotate Image</label>
|
||||
<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 = 250;
|
||||
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;
|
||||
});
|
||||
}
|
||||
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 (!page.croppa.zoomOut) return;
|
||||
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;
|
||||
}
|
||||
if (!this.pages[this.pages.length - 1].base64) {
|
||||
this.notify({
|
||||
message: "You last page is empty. Delete or update",
|
||||
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 onEditClicked() {
|
||||
const lastPage = this.currentPage;
|
||||
if (lastPage >= 0) {
|
||||
const imageBlob = await this.pages[lastPage].croppa.promisedBlob(
|
||||
MIME_TYPE,
|
||||
COMPRESSION_RATE
|
||||
);
|
||||
if (!imageBlob) {
|
||||
this.notify({
|
||||
message: "Cant have an empty page.",
|
||||
level: "warning"
|
||||
});
|
||||
this.currentPage = lastPage;
|
||||
return;
|
||||
}
|
||||
let url = URL.createObjectURL(imageBlob);
|
||||
this.pages[lastPage].base64 = this.pages[
|
||||
lastPage
|
||||
].croppa.generateDataUrl(MIME_TYPE, COMPRESSION_RATE);
|
||||
this.pages[lastPage].image = url;
|
||||
this.currentPage = -7;
|
||||
this.editMode = false;
|
||||
} else {
|
||||
this.notify({
|
||||
message: `Please add pages before preview`,
|
||||
level: "warning"
|
||||
});
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
this.editMode = true;
|
||||
let url = URL.createObjectURL(imageBlob);
|
||||
this.pages[lastPage].base64 = this.pages[
|
||||
lastPage
|
||||
].croppa.generateDataUrl(MIME_TYPE, COMPRESSION_RATE);
|
||||
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;
|
||||
},
|
||||
zoom(zoomIn: boolean) {
|
||||
const amount = zoomIn ? 2 : -2;
|
||||
this.pageZoom = Number(this.pageZoom) + amount;
|
||||
},
|
||||
blurZoomSlider() {
|
||||
const slider: HTMLInputElement = this.$refs.zoomRangeSlider;
|
||||
if (slider) {
|
||||
slider.blur();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["user"]),
|
||||
//
|
||||
bookPages() {
|
||||
return this.pages.slice(1);
|
||||
},
|
||||
croppaClass() {
|
||||
if (this.currentPage != 0 && this.currentPage % 2 == 0) {
|
||||
return `book-stitch-preview-${this.book.ltr ? "left" : "right"}`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
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,
|
||||
DEFAULT_ZOOM
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -138,8 +138,13 @@
|
|||
<h2 class="subtitle">
|
||||
<i class="fa fa-fw fa-book"></i> My Books
|
||||
</h2>
|
||||
<div class="is-flex m-b-md is-justify-centered">
|
||||
<div class="book-thumb m-l-md" v-for="book in user.books" :key="book.id">
|
||||
<div class="is-flex m-b-md is-justify-centered has-wrap">
|
||||
<div
|
||||
class="book-thumb enabled m-l-md"
|
||||
v-for="book in user.books"
|
||||
:key="book.id"
|
||||
@click="goToBook(book)"
|
||||
>
|
||||
<div class="book-cover">
|
||||
<figure class="image is-2by3 m-a">
|
||||
<img :src="`/u/books/${book.id}/thumbnail`" />
|
||||
|
@ -230,6 +235,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`
|
||||
|
@ -243,6 +251,9 @@ export default {
|
|||
goChildProfile(connection) {
|
||||
this.$router.push({ path: `/child/${connection.id}` });
|
||||
},
|
||||
goToBook(book) {
|
||||
this.$router.push({ path: `/book/${book.id}` });
|
||||
},
|
||||
async onChildCreated(child) {
|
||||
this.loading = true;
|
||||
await this.getUser();
|
||||
|
|
|
@ -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}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUser(payload: { name?: string; avatar?: string; profile_cover?: string; email?: string }) {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
|
|
|
@ -42,6 +42,7 @@ const globalMiddleware =
|
|||
auth: 'Adonis/Middleware/Auth',
|
||||
guest: 'Adonis/Middleware/AllowGuestOnly',
|
||||
adminAuth: 'App/Middleware/AdminAuth',
|
||||
BookCallPageAuth: 'App/Middleware/BookCallPageAuth',
|
||||
BookPageAuth: 'App/Middleware/BookPageAuth',
|
||||
BookContext: 'App/Middleware/BookContext',
|
||||
CallContext: 'App/Middleware/CallContext',
|
||||
|
|
|
@ -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']);
|
||||
|
@ -60,7 +61,9 @@ Route
|
|||
.get(
|
||||
'/u/call/:callId/books/:bookId/page/:pageNumber',
|
||||
'BookApiController.getPage')
|
||||
.middleware(['auth', 'BookContext', 'CallContext', 'BookPageAuth']);
|
||||
.middleware(['auth', 'BookContext', 'CallContext', 'BookCallPageAuth']);
|
||||
Route.get('/u/books/:bookId/page/:pageNumber', 'BookApiController.getPage')
|
||||
.middleware(['auth', 'BookContext', 'BookPageAuth']);
|
||||
/**
|
||||
* Public book thumbnail
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue