Browse Source

Merge branch 'release/0.11.8'

Book Pauk 3 years ago
parent
commit
7b59f911ef

+ 30 - 3
client/api/reader.js

@@ -1,5 +1,6 @@
 import axios from 'axios';
 import * as utils from '../share/utils';
+import * as cryptoUtils from '../share/cryptoUtils';
 import wsc from './webSocketConnection';
 
 const api = axios.create({
@@ -174,11 +175,10 @@ class Reader {
         return await axios.get(url, options);
     }
 
-    async uploadFile(file, maxUploadFileSize, callback) {
-        if (!maxUploadFileSize)
-            maxUploadFileSize = 10*1024*1024;
+    async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
         if (file.size > maxUploadFileSize)
             throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
+
         let formData = new FormData();
         formData.append('file', file, file.name);
 
@@ -225,6 +225,33 @@ class Reader {
 
         return response;
     }
+
+    async uploadFileBuf(buf, urlCallback) {
+        const key = utils.toHex(cryptoUtils.sha256(buf));
+        const url = `disk://${key}`;
+
+        if (urlCallback)
+            urlCallback(url);
+
+        let response;
+        try {
+            await axios.head(`/upload/${key}`, {headers: {'Cache-Control': 'no-cache'}});
+            response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
+        } catch (e) {
+            response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
+        }
+
+        if (response.error)
+            throw new Error(response.error);
+
+        return response;
+    }
+
+    async getUploadedFileBuf(url) {
+        url = url.replace('disk://', '/upload/');
+        return (await axios.get(url)).data;
+    }
+
 }
 
 export default new Reader();

+ 32 - 4
client/components/Reader/Reader.vue

@@ -194,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
 
 import bookManager from './share/bookManager';
 import wallpaperStorage from './share/wallpaperStorage';
+import coversStorage from './share/coversStorage';
 import dynamicCss from '../../share/dynamicCss';
 
 import rstore from '../../store/modules/reader';
@@ -366,6 +367,8 @@ class Reader {
     mounted() {
         (async() => {
             await wallpaperStorage.init();
+            await coversStorage.init();
+            
             await bookManager.init(this.settings);
             bookManager.addEventListener(this.bookManagerEvent);
 
@@ -450,22 +453,47 @@ class Reader {
 
     //wallpaper css
     async loadWallpapers() {
-        const wallpaperDataLength = await wallpaperStorage.getLength();
-        if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
-            this.wallpaperDataLength = wallpaperDataLength;
+        if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
+            this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
 
             let newCss = '';
+            let updated = false;
+            const wallpaperExists = new Set();
             for (const wp of this.userWallpapers) {
-                const data = await wallpaperStorage.getData(wp.cssClass);
+                wallpaperExists.add(wp.cssClass);
 
+                let data = await wallpaperStorage.getData(wp.cssClass);
                 if (!data) {
                     //здесь будем восстанавливать данные с сервера
+                    const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
+                    try {
+                        data = await readerApi.getUploadedFileBuf(url);
+                        await wallpaperStorage.setData(wp.cssClass, data);
+                        updated = true;
+                    } catch (e) {
+                        console.error(e);
+                    }
                 }
 
                 if (data) {
                     newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;                
                 }
             }
+
+            //почистим wallpaperStorage
+            for (const key of await wallpaperStorage.getKeys()) {
+                if (!wallpaperExists.has(key)) {
+                    await wallpaperStorage.removeData(key);
+                }
+            }
+
+            //обновим settings, если загружали обои из /upload/
+            if (updated) {
+                const newSettings = _.cloneDeep(this.settings);
+                newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
+                this.commit('reader/setSettings', newSettings);
+            }
+
             dynamicCss.replace('wallpapers', newCss);
         }
     }

+ 61 - 8
client/components/Reader/RecentBooksPage/RecentBooksPage.vue

@@ -105,8 +105,9 @@
                     </div>
 
                     <div class="row-part column justify-center items-stretch" style="width: 80px">
-                        <div class="col row justify-center items-center clickable" @click="loadBook(item)">
-                            <q-icon name="la la-book" size="40px" style="color: #dddddd" />
+                        <div class="col row justify-center items-center clickable" style="padding: 4px" @click="loadBook(item)">
+                            <div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
+                            <q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
                         </div>
 
                         <div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
@@ -213,6 +214,7 @@ import LockQueue from '../../../share/LockQueue';
 import Window from '../../share/Window.vue';
 import bookManager from '../share/bookManager';
 import readerApi from '../../../api/reader';
+import coversStorage from '../share/coversStorage';
 
 const componentOptions = {
     components: {
@@ -240,6 +242,8 @@ class RecentBooksPage {
     showSameBook = false;
     archive = false;
 
+    covers = {};
+
     created() {
         this.commit = this.$store.commit;
 
@@ -264,6 +268,7 @@ class RecentBooksPage {
             this.showBar();
             await this.updateTableData();
             await this.scrollToActiveBook();
+            //await this.scrollRefresh();
         })();
     }
 
@@ -336,6 +341,7 @@ class RecentBooksPage {
                     active: (activeBook.key == book.key),
                     activeParent: false,
                     inGroup: false,
+                    coverPageUrl: book.coverPageUrl,
 
                     //для сортировки
                     loadTimeRaw,
@@ -435,8 +441,6 @@ class RecentBooksPage {
             //.....
 
             this.tableData = result;
-            
-            this.$refs.virtualScroll.refresh();
         } finally {
             this.lock.ret();
         }
@@ -569,6 +573,8 @@ class RecentBooksPage {
     }
 
     async scrollToActiveBook() {
+        await this.$nextTick();
+
         this.lockScroll = true;
         try {
             let activeIndex = -1;
@@ -614,6 +620,16 @@ class RecentBooksPage {
         }
     }
 
+    async scrollRefresh() {
+        this.lockScroll = true;
+        await utils.sleep(100);
+        try {
+            this.$refs.virtualScroll.refresh();
+        } finally {
+            await utils.sleep(100);
+            this.lockScroll = false;
+        }
+    }
 
     get sortMethodOptions() {
         return [
@@ -643,6 +659,43 @@ class RecentBooksPage {
         }
         return true;
     }
+
+    makeCoverHtml(data) {
+        return `<img src="${data}" style="height: 100%; width: 100%; object-fit: contain" />`;
+    }
+
+    isLoadedCover(coverPageUrl) {
+        if (!coverPageUrl)
+            return false;
+
+        let loadedCover = this.covers[coverPageUrl];
+        if (!loadedCover) {
+            (async() => {
+                //сначала заглянем в storage
+                let data = await coversStorage.getData(coverPageUrl);
+                if (data) {
+                   this.covers[coverPageUrl] = this.makeCoverHtml(data);
+                } else {//иначе идем на сервер
+                    try {
+                        data = await readerApi.getUploadedFileBuf(coverPageUrl);
+                        await coversStorage.setData(coverPageUrl, data);
+                        this.covers[coverPageUrl] = this.makeCoverHtml(data);
+                    } catch (e) {
+                        console.error(e);
+                    }
+                }
+            })();
+        }
+
+        return (loadedCover != undefined);
+    }
+
+    getCoverHtml(coverPageUrl) {
+        if (coverPageUrl && this.covers[coverPageUrl])
+            return this.covers[coverPageUrl];
+        else
+            return '';
+    }
 }
 
 export default vueComponent(RecentBooksPage);
@@ -716,14 +769,14 @@ export default vueComponent(RecentBooksPage);
     line-height: 110%;
     border-left: 1px solid #cccccc;
     border-bottom: 1px solid #cccccc;
-    height: 12px;
+    height: 14px;
 }
 
 .row-info-top {
     line-height: 110%;
     border: 1px solid #cccccc;
     border-right: 0;
-    height: 12px;
+    height: 14px;
 }
 
 .time-info, .row-info-top {
@@ -731,8 +784,8 @@ export default vueComponent(RecentBooksPage);
 }
 
 .read-bar {
-    height: 4px;
-    background-color: #bbbbbb;
+    height: 6px;
+    background-color: #b8b8b8;
 }
 
 .del-button {

+ 11 - 1
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -124,6 +124,7 @@ import NumInput from '../../share/NumInput.vue';
 import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
 import wallpaperStorage from '../share/wallpaperStorage';
 
+import readerApi from '../../../api/reader';
 import rstore from '../../../store/modules/reader';
 import defPalette from './defPalette';
 
@@ -636,8 +637,17 @@ class SettingsPage {
 
                     if (index < 0)
                         newUserWallpapers.push({label, cssClass});
-                    if (!wallpaperStorage.keyExists(cssClass))
+                    if (!wallpaperStorage.keyExists(cssClass)) {
                         await wallpaperStorage.setData(cssClass, data);
+                        //отправим data на сервер в файл `/upload/${key}`
+                        try {
+                            //const res = 
+                            await readerApi.uploadFileBuf(data);
+                            //console.log(res);
+                        } catch (e) {
+                            console.error(e);
+                        }
+                    }
 
                     this.userWallpapers = newUserWallpapers;
                     this.wallpaper = cssClass;

+ 7 - 1
client/components/Reader/share/BookParser.js

@@ -85,6 +85,7 @@ export default class BookParser {
         let binaryId = '';
         let binaryType = '';
         let dimPromises = [];
+        this.coverPageId = '';
 
         //оглавление
         this.contents = [];
@@ -289,7 +290,7 @@ export default class BookParser {
                     const href = attrs.href.value;
                     const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
                     const {id, local} = this.imageHrefToId(href);
-                    if (href[0] == '#') {//local
+                    if (local) {//local
                         imageNum++;
 
                         if (inPara && !this.sets.showInlineImagesInCenter && !center)
@@ -301,6 +302,11 @@ export default class BookParser {
 
                         if (inPara && this.sets.showInlineImagesInCenter)
                             newParagraph();
+
+                        //coverpage
+                        if (path == '/fictionbook/description/title-info/coverpage/image') {
+                            this.coverPageId = id;
+                        }
                     } else {//external
                         imageNum++;
 

+ 30 - 1
client/components/Reader/share/bookManager.js

@@ -2,8 +2,10 @@ import localForage from 'localforage';
 import path from 'path-browserify';
 import _ from 'lodash';
 
-import * as utils from '../../../share/utils';
 import BookParser from './BookParser';
+import readerApi from '../../../api/reader';
+import coversStorage from './coversStorage';
+import * as utils from '../../../share/utils';
 
 const maxDataSize = 500*1024*1024;//compressed bytes
 const maxRecentLength = 5000;
@@ -345,9 +347,36 @@ class BookManager {
         const parsed = new BookParser(this.settings);
 
         const parsedMeta = await parsed.parse(data, callback);
+
+        //cover page
+        let coverPageUrl = '';
+        if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
+            const bin = parsed.binary[parsed.coverPageId];
+            let dataUrl = `data:${bin.type};base64,${bin.data}`;
+            try {
+                dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
+            } catch (e) {
+                console.error(e);
+            }
+
+            //отправим dataUrl на сервер в /upload
+            try {
+                await readerApi.uploadFileBuf(dataUrl, (url) => {
+                    coverPageUrl = url;
+                });
+            } catch (e) {
+                console.error(e);
+            }
+
+            //сохраним в storage
+            if (coverPageUrl)
+                await coversStorage.setData(coverPageUrl, dataUrl);
+        }
+
         const result = Object.assign({}, meta, parsedMeta, {
             length: data.length,
             textLength: parsed.textLength,
+            coverPageUrl,
             parsed
         });
 

+ 61 - 0
client/components/Reader/share/coversStorage.js

@@ -0,0 +1,61 @@
+import localForage from 'localforage';
+//import _ from 'lodash';
+import * as utils from '../../../share/utils';
+
+const maxDataSize = 100*1024*1024;
+
+const coversStore = localForage.createInstance({
+    name: 'coversStorage'
+});
+
+class CoversStorage {
+    constructor() {
+    }
+
+    async init() {
+        this.cleanCovers(); //no await
+    }
+
+    async setData(key, data) {
+        await coversStore.setItem(key, {addTime: Date.now(), data});
+    }
+
+    async getData(key) {
+        const item = await coversStore.getItem(key);
+        return (item ? item.data : undefined);
+    }
+
+    async removeData(key) {
+        await coversStore.removeItem(key);
+    }
+
+    async cleanCovers() {
+        await utils.sleep(10000);
+
+        while (1) {// eslint-disable-line no-constant-condition
+            let size = 0;
+            let min = Date.now();
+            let toDel = null;
+            for (const key of (await coversStore.keys())) {
+                const item = await coversStore.getItem(key);
+
+                size += item.data.length;
+
+                if (item.addTime < min) {
+                    toDel = key;
+                    min = item.addTime;
+                }
+            }
+
+
+            if (size > maxDataSize && toDel) {
+                await this.removeData(toDel);
+            } else {
+                break;
+            }
+        }
+    }
+
+}
+
+export default new CoversStorage();

+ 4 - 0
client/components/Reader/share/wallpaperStorage.js

@@ -32,6 +32,10 @@ class WallpaperStorage {
         this.cachedKeys = await wpStore.keys();
     }
 
+    async getKeys() {
+        return await wpStore.keys();
+    }
+
     keyExists(key) {//не асинхронная
         return this.cachedKeys.includes(key);
     }

+ 14 - 0
client/components/Reader/versionHistory.js

@@ -1,4 +1,18 @@
 export const versionHistory = [
+{
+    version: '0.11.8',
+    releaseDate: '2022-07-14',
+    showUntil: '2022-07-13',
+    content:
+`
+<ul>
+    <li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
+    <li>добавлена синхронизация обоев</li>
+</ul>
+
+`
+},
+
 {
     version: '0.11.7',
     releaseDate: '2022-07-12',

+ 46 - 0
client/share/utils.js

@@ -363,4 +363,50 @@ export function getBookTitle(fb2) {
     ]).join(' - ');
 
     return result;
+}
+
+export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
+    return new Promise ((resolve, reject) => { (async() => {
+        const img = new Image();
+
+        let resolved = false;
+        img.onload = () => {
+            try {
+                let width = img.width;
+                let height = img.height;
+
+                if (width > height) {
+                    if (width > toWidth) {
+                        height = height * (toWidth / width);
+                        width = toWidth;
+                    }
+                } else {
+                    if (height > toHeight) {
+                        width = width * (toHeight / height);
+                        height = toHeight;
+                    }
+                }
+
+                const canvas = document.createElement('canvas');
+                canvas.width = width;
+                canvas.height = height;
+                const ctx = canvas.getContext('2d');
+                ctx.drawImage(img, 0, 0, width, height);
+                const result = canvas.toDataURL('image/jpeg', quality);
+                resolved = true;
+                resolve(result);
+            } catch (e) {
+                reject(e);
+                return;
+            }
+        };
+
+        img.onerror = reject;
+
+        img.src = dataUrl;
+
+        await sleep(1000);
+        if (!resolved)
+            reject('Не удалось изменить размер');
+    })().catch(reject); });
 }

+ 2 - 0
client/store/modules/reader.js

@@ -191,6 +191,8 @@ const settingDefaults = {
 
     recentShowSameBook: false,
     recentSortMethod: '',
+
+    needUpdateSettingsView: 0,
 };
 
 for (const font of fonts)

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "Liberama",
-  "version": "0.11.7",
+  "version": "0.11.8",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "Liberama",
-      "version": "0.11.7",
+      "version": "0.11.8",
       "hasInstallScript": true,
       "license": "CC0-1.0",
       "dependencies": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.11.7",
+  "version": "0.11.8",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",

+ 22 - 0
server/controllers/WebSocketController.js

@@ -25,6 +25,10 @@ class WebSocketController {
             ws.on('message', (message) => {
                 this.onMessage(ws, message.toString());
             });
+
+            ws.on('error', (err) => {
+                log(LM_ERR, err);
+            });
         });
 
         setTimeout(() => { this.periodicClean(); }, cleanPeriod);
@@ -70,6 +74,10 @@ class WebSocketController {
                     await this.readerRestoreCachedFile(req, ws); break;
                 case 'reader-storage':
                     await this.readerStorageDo(req, ws); break;
+                case 'upload-file-buf':
+                    await this.uploadFileBuf(req, ws); break;
+                case 'upload-file-touch':
+                    await this.uploadFileTouch(req, ws); break;
 
                 default:
                     throw new Error(`Action not found: ${req.action}`);
@@ -168,6 +176,20 @@ class WebSocketController {
 
         this.send(await this.readerStorage.doAction(req.body), req, ws);
     }
+
+    async uploadFileBuf(req, ws) {
+        if (!req.buf)
+            throw new Error(`key 'buf' is empty`);
+        
+        this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
+    }
+
+    async uploadFileTouch(req, ws) {
+        if (!req.url)
+            throw new Error(`key 'url' is empty`);
+        
+        this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
+    }
 }
 
 module.exports = WebSocketController;

+ 21 - 0
server/core/Reader/ReaderWorker.js

@@ -219,6 +219,27 @@ class ReaderWorker {
         return `disk://${hash}`;
     }
 
+    async saveFileBuf(buf) {
+        const hash = await utils.getBufHash(buf, 'sha256', 'hex');
+        const outFilename = `${this.config.uploadDir}/${hash}`;
+
+        if (!await fs.pathExists(outFilename)) {
+            await fs.writeFile(outFilename, buf);
+        } else {
+            await utils.touchFile(outFilename);
+        }
+
+        return `disk://${hash}`;
+    }
+
+    async uploadFileTouch(url) {
+        const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
+
+        await utils.touchFile(outFilename);
+
+        return url;
+    }
+
     async restoreRemoteFile(filename) {
         const basename = path.basename(filename);
         const targetName = `${this.config.tempPublicDir}/${basename}`;

+ 1 - 1
server/core/WebSocketConnection.js

@@ -94,7 +94,7 @@ class WebSocketConnection {
                     this.ws = new this.WebSocket(this.url);
                 }
 
-                const onopen = (e) => {
+                const onopen = () => {
                     this.connecting = false;
                     resolve(this.ws);
                 };

+ 7 - 0
server/core/utils.js

@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
     });
 }
 
+function getBufHash(buf, hashName, enc) {
+    const hash = crypto.createHash(hashName);
+    hash.update(buf);
+    return hash.digest(enc);
+}
+
 function sleep(ms) {
     return new Promise(resolve => setTimeout(resolve, ms));
 }
@@ -129,6 +135,7 @@ module.exports = {
     fromBase36,
     bufferRemoveZeroes,
     getFileHash,
+    getBufHash,
     sleep,
     toUnixTime,
     randomHexString,

+ 4 - 2
server/index.js

@@ -11,6 +11,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
 
 let log = null;
 
+const maxPayloadSize = 50;//in MB
+
 async function init() {
     //config
     const configManager = new (require('./config'))();//singleton
@@ -63,7 +65,7 @@ async function main() {
         if (serverCfg.mode !== 'none') {
             const app = express();
             const server = http.createServer(app);
-            const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
+            const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
 
             const serverConfig = Object.assign({}, config, serverCfg);
 
@@ -75,7 +77,7 @@ async function main() {
             }
 
             app.use(compression({ level: 1 }));
-            app.use(express.json({limit: '10mb'}));
+            app.use(express.json({limit: `${maxPayloadSize}mb`}));
             if (devModule)
                 devModule.logQueries(app);