diff --git a/build/webpack.prod.config.js b/build/webpack.prod.config.js index cfc04abc..c8472dc2 100644 --- a/build/webpack.prod.config.js +++ b/build/webpack.prod.config.js @@ -32,7 +32,15 @@ module.exports = merge(baseWpConfig, { }, optimization: { minimizer: [ - new TerserPlugin(), + new TerserPlugin({ + cache: true, + parallel: true, + terserOptions: { + output: { + comments: false, + }, + }, + }), new OptimizeCSSAssetsPlugin() ] }, diff --git a/client/api/reader.js b/client/api/reader.js index c89ccec0..4e7a99da 100644 --- a/client/api/reader.js +++ b/client/api/reader.js @@ -1,5 +1,8 @@ +import _ from 'lodash'; import axios from 'axios'; -import {sleep} from '../share/utils'; +import {Buffer} from 'safe-buffer'; + +import * as utils from '../share/utils'; const api = axios.create({ baseURL: '/api/reader' @@ -41,7 +44,7 @@ class Reader { throw new Error(errMes); } if (i > 0) - await sleep(refreshPause); + await utils.sleep(refreshPause); i++; if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера @@ -106,6 +109,16 @@ class Reader { return url; } + + async storage(request) { + let response = await api.post('/storage', request); + + const state = response.data.state; + if (!state) + throw new Error('Неверный ответ api'); + + return response.data; + } } export default new Reader(); \ No newline at end of file diff --git a/client/components/Reader/HistoryPage/HistoryPage.vue b/client/components/Reader/HistoryPage/HistoryPage.vue index 176b56e8..04a87888 100644 --- a/client/components/Reader/HistoryPage/HistoryPage.vue +++ b/client/components/Reader/HistoryPage/HistoryPage.vue @@ -127,7 +127,6 @@ class HistoryPage extends Vue { init() { this.updateTableData(); - this.mostRecentBook = bookManager.mostRecentBook(); this.$nextTick(() => { this.$refs.input.focus(); }); @@ -141,9 +140,11 @@ class HistoryPage extends Vue { let result = []; const sorted = bookManager.getSortedRecent(); - const len = (sorted.length < 100 ? sorted.length : 100); - for (let i = 0; i < len; i++) { + for (let i = 0; i < sorted.length; i++) { const book = sorted[i]; + if (book.deleted) + continue; + let d = new Date(); d.setTime(book.touchTime); const t = formatDate(d).split(' '); @@ -164,11 +165,21 @@ class HistoryPage extends Vue { else title = ''; - let author = _.compact([ - fb2.lastName, - fb2.firstName, - fb2.middleName - ]).join(' '); + let author = ''; + if (fb2.author) { + const authorNames = fb2.author.map(a => _.compact([ + a.lastName, + a.firstName, + a.middleName + ]).join(' ')); + author = authorNames.join(', '); + } else { + author = _.compact([ + fb2.lastName, + fb2.firstName, + fb2.middleName + ]).join(' '); + } author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url)); result.push({ @@ -183,6 +194,8 @@ class HistoryPage extends Vue { path: book.path, key: book.key, }); + if (result.length >= 100) + break; } const search = this.search; @@ -225,13 +238,7 @@ class HistoryPage extends Vue { await bookManager.delRecentBook({key}); this.updateTableData(); - const newRecent = bookManager.mostRecentBook(); - - if (!(this.mostRecentBook && newRecent && this.mostRecentBook.key == newRecent.key)) - this.$emit('load-book', newRecent); - - this.mostRecentBook = newRecent; - if (!this.mostRecentBook) + if (!bookManager.mostRecentBook()) this.close(); } diff --git a/client/components/Reader/Reader.vue b/client/components/Reader/Reader.vue index efb833a2..bbef9b77 100644 --- a/client/components/Reader/Reader.vue +++ b/client/components/Reader/Reader.vue @@ -73,6 +73,7 @@ + @@ -81,6 +82,9 @@ //----------------------------------------------------------------------------- import Vue from 'vue'; import Component from 'vue-class-component'; +import _ from 'lodash'; +import {Buffer} from 'safe-buffer'; + import LoaderPage from './LoaderPage/LoaderPage.vue'; import TextPage from './TextPage/TextPage.vue'; import ProgressPage from './ProgressPage/ProgressPage.vue'; @@ -92,12 +96,11 @@ import HistoryPage from './HistoryPage/HistoryPage.vue'; import SettingsPage from './SettingsPage/SettingsPage.vue'; import HelpPage from './HelpPage/HelpPage.vue'; import ClickMapPage from './ClickMapPage/ClickMapPage.vue'; +import ServerStorage from './ServerStorage/ServerStorage.vue'; import bookManager from './share/bookManager'; import readerApi from '../../api/reader'; -import _ from 'lodash'; -import {sleep} from '../../share/utils'; -import restoreOldSettings from './share/restoreOldSettings'; +import * as utils from '../../share/utils'; export default @Component({ components: { @@ -112,6 +115,7 @@ export default @Component({ SettingsPage, HelpPage, ClickMapPage, + ServerStorage, }, watch: { bookPos: function(newValue) { @@ -166,6 +170,7 @@ class Reader extends Vue { actionList = []; actionCur = -1; + hidden = false; created() { this.loading = true; @@ -192,6 +197,18 @@ class Reader extends Vue { } }, 500); + this.debouncedSaveRecent = _.debounce(async() => { + const serverStorage = this.$refs.serverStorage; + while (!serverStorage.inited) await utils.sleep(1000); + await serverStorage.saveRecent(); + }, 1000); + + this.debouncedSaveRecentLast = _.debounce(async() => { + const serverStorage = this.$refs.serverStorage; + while (!serverStorage.inited) await utils.sleep(1000); + await serverStorage.saveRecentLast(); + }, 1000); + document.addEventListener('fullscreenchange', () => { this.fullScreenActive = (document.fullscreenElement !== null); }); @@ -202,15 +219,17 @@ class Reader extends Vue { mounted() { (async() => { await bookManager.init(this.settings); - await restoreOldSettings(this.settings, bookManager, this.commit); + bookManager.addEventListener(this.bookManagerEvent); if (this.$root.rootRoute == '/reader') { if (this.routeParamUrl) { - this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos}); + await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos}); } else { this.loaderActive = true; } } + + this.checkSetStorageAccessKey(); this.loading = false; })(); } @@ -224,6 +243,20 @@ class Reader extends Vue { this.blinkCachedLoad = settings.blinkCachedLoad; } + checkSetStorageAccessKey() { + const q = this.$route.query; + + if (q['setStorageAccessKey']) { + this.$router.replace(`/reader`); + this.settingsToggle(); + this.$nextTick(() => { + this.$refs.settingsPage.enterServerStorageKey( + Buffer.from(utils.fromBase58(q['setStorageAccessKey'])).toString() + ); + }); + } + } + get routeParamPos() { let result = undefined; const q = this.$route.query; @@ -237,6 +270,8 @@ class Reader extends Vue { } updateRoute(isNewRoute) { + if (this.loading) + return; const recent = this.mostRecentBook(); const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : ''); const url = (recent ? `url=${recent.url}` : ''); @@ -265,6 +300,45 @@ class Reader extends Vue { this.debouncedUpdateRoute(); } + async bookManagerEvent(eventName) { + const serverStorage = this.$refs.serverStorage; + if (eventName == 'load-meta-finish') { + serverStorage.init(); + const result = await bookManager.cleanRecentBooks(); + if (result) + this.debouncedSaveRecent(); + } + + if (eventName == 'recent-changed' || eventName == 'save-recent') { + if (this.historyActive) { + this.$refs.historyPage.updateTableData(); + } + + const oldBook = this.mostRecentBookReactive; + const newBook = bookManager.mostRecentBook(); + + if (oldBook && newBook) { + if (oldBook.key != newBook.key) { + this.loadingBook = true; + try { + await this.loadBook(newBook); + } finally { + this.loadingBook = false; + } + } else if (oldBook.bookPos != newBook.bookPos) { + while (this.loadingBook) await utils.sleep(100); + this.bookPosChanged({bookPos: newBook.bookPos}); + } + } + + if (eventName == 'recent-changed') { + this.debouncedSaveRecentLast(); + } else { + this.debouncedSaveRecent(); + } + } + } + get toolBarActive() { return this.reader.toolBarActive; } @@ -584,6 +658,11 @@ class Reader extends Vue { this.$root.$emit('set-app-title'); } + // на LoaderPage всегда показываем toolBar + if (result == 'LoaderPage' && !this.toolBarActive) { + this.toolBarToggle(); + } + if (this.lastActivePage != result && result == 'TextPage') { //акивируем страницу с текстом this.$nextTick(async() => { @@ -609,7 +688,7 @@ class Reader extends Vue { return result; } - loadBook(opts) { + async loadBook(opts) { if (!opts || !opts.url) { this.mostRecentBook(); return; @@ -628,119 +707,120 @@ class Reader extends Vue { } this.progressActive = true; - this.$nextTick(async() => { - const progress = this.$refs.page; - this.actionList = []; - this.actionCur = -1; + await this.$nextTick() - try { - progress.show(); - progress.setState({state: 'parse'}); + const progress = this.$refs.page; - // есть ли среди недавних - const key = bookManager.keyFromUrl(url); - let wasOpened = await bookManager.getRecentBook({key}); - wasOpened = (wasOpened ? wasOpened : {}); - const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); - const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); - const bookPosPercent = wasOpened.bookPosPercent; + this.actionList = []; + this.actionCur = -1; - let book = null; + try { + progress.show(); + progress.setState({state: 'parse'}); - if (!opts.force) { - // пытаемся загрузить и распарсить книгу в менеджере из локального кэша - const bookParsed = await bookManager.getBook({url}, (prog) => { - progress.setState({progress: prog}); - }); + // есть ли среди недавних + const key = bookManager.keyFromUrl(url); + let wasOpened = await bookManager.getRecentBook({key}); + wasOpened = (wasOpened ? wasOpened : {}); + const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); + const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); - // если есть в локальном кэше - if (bookParsed) { - await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, bookParsed)); - this.mostRecentBook(); - this.addAction(bookPos); - this.loaderActive = false; - progress.hide(); this.progressActive = false; - this.blinkCachedLoadMessage(); + let book = null; - await this.activateClickMapPage(); - return; - } - - // иначе идем на сервер - // пытаемся загрузить готовый файл с сервера - if (wasOpened.path) { - try { - const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => { - progress.setState(state); - }); - book = Object.assign({}, wasOpened, {data: resp.data}); - } catch (e) { - //молчим - } - } - } - - progress.setState({totalSteps: 5}); - - // не удалось, скачиваем книгу полностью с конвертацией - let loadCached = true; - if (!book) { - book = await readerApi.loadBook(url, (state) => { - progress.setState(state); - }); - loadCached = false; - } - - // добавляем в bookManager - progress.setState({state: 'parse', step: 5}); - const addedBook = await bookManager.addBook(book, (prog) => { + if (!opts.force) { + // пытаемся загрузить и распарсить книгу в менеджере из локального кэша + const bookParsed = await bookManager.getBook({url}, (prog) => { progress.setState({progress: prog}); }); - // добавляем в историю - await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, addedBook)); - this.mostRecentBook(); - this.addAction(bookPos); - this.updateRoute(true); - - this.loaderActive = false; - progress.hide(); this.progressActive = false; - if (loadCached) { + // если есть в локальном кэше + if (bookParsed) { + await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed)); + this.mostRecentBook(); + this.addAction(bookPos); + this.loaderActive = false; + progress.hide(); this.progressActive = false; this.blinkCachedLoadMessage(); - } else - this.stopBlink = true; - await this.activateClickMapPage(); - } catch (e) { - progress.hide(); this.progressActive = false; - this.loaderActive = true; - this.$alert(e.message, 'Ошибка', {type: 'error'}); + await this.activateClickMapPage(); + return; + } + + // иначе идем на сервер + // пытаемся загрузить готовый файл с сервера + if (wasOpened.path) { + try { + const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => { + progress.setState(state); + }); + book = Object.assign({}, wasOpened, {data: resp.data}); + } catch (e) { + //молчим + } + } } - }); - } - loadFile(opts) { - this.progressActive = true; - this.$nextTick(async() => { - const progress = this.$refs.page; - try { - progress.show(); - progress.setState({state: 'upload'}); + progress.setState({totalSteps: 5}); - const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => { + // не удалось, скачиваем книгу полностью с конвертацией + let loadCached = true; + if (!book) { + book = await readerApi.loadBook(url, (state) => { progress.setState(state); }); - - progress.hide(); this.progressActive = false; - - this.loadBook({url}); - } catch (e) { - progress.hide(); this.progressActive = false; - this.loaderActive = true; - this.$alert(e.message, 'Ошибка', {type: 'error'}); + loadCached = false; } - }); + + // добавляем в bookManager + progress.setState({state: 'parse', step: 5}); + const addedBook = await bookManager.addBook(book, (prog) => { + progress.setState({progress: prog}); + }); + + // добавляем в историю + await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook)); + this.mostRecentBook(); + this.addAction(bookPos); + this.updateRoute(true); + + this.loaderActive = false; + progress.hide(); this.progressActive = false; + if (loadCached) { + this.blinkCachedLoadMessage(); + } else + this.stopBlink = true; + + await this.activateClickMapPage(); + } catch (e) { + progress.hide(); this.progressActive = false; + this.loaderActive = true; + this.$alert(e.message, 'Ошибка', {type: 'error'}); + } + } + + async loadFile(opts) { + this.progressActive = true; + + await this.$nextTick(); + + const progress = this.$refs.page; + try { + progress.show(); + progress.setState({state: 'upload'}); + + const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => { + progress.setState(state); + }); + + progress.hide(); this.progressActive = false; + + await this.loadBook({url}); + } catch (e) { + progress.hide(); this.progressActive = false; + this.loaderActive = true; + this.$alert(e.message, 'Ошибка', {type: 'error'}); + } } blinkCachedLoadMessage() { @@ -757,7 +837,7 @@ class Reader extends Vue { this.showRefreshIcon = !this.showRefreshIcon; if (page.blinkCachedLoadMessage) page.blinkCachedLoadMessage(this.showRefreshIcon); - await sleep(500); + await utils.sleep(500); if (this.stopBlink) break; this.blinkCount--; diff --git a/client/components/Reader/ServerStorage/ServerStorage.vue b/client/components/Reader/ServerStorage/ServerStorage.vue new file mode 100644 index 00000000..29a81c94 --- /dev/null +++ b/client/components/Reader/ServerStorage/ServerStorage.vue @@ -0,0 +1,611 @@ + + + diff --git a/client/components/Reader/SettingsPage/SettingsPage.vue b/client/components/Reader/SettingsPage/SettingsPage.vue index e24ac197..9814d6fa 100644 --- a/client/components/Reader/SettingsPage/SettingsPage.vue +++ b/client/components/Reader/SettingsPage/SettingsPage.vue @@ -7,7 +7,106 @@ - + + + +
Управление синхронизацией данных
+ + Включить синхронизацию с сервером + +
+ +
+ +
Профили устройств
+ + +
+ Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером. +
При выборе "Нет" синхронизация настроек (но не книг) отключается. +
+
+ + + + + + + + + + + Добавить + Удалить + Удалить все + +
+ + +
Ключ доступа
+ + +
+ Ключ доступа позволяет восстановить профили с настройками и список читаемых книг. + Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом. +
+
+ + + + Скрыть + Показать + ключ доступа + + + + +
+
+ {{ partialStorageKey }} (часть вашего ключа) +
+
+
+
+
{{ serverStorageKey }}
+
+ Скопировать ключ +
+
+
Переход по ссылке позволит автоматически ввести ключ доступа: +
+
+ Скопировать ссылку +
+
+
+
+
+ + + Ввести ключ доступа + + + Сгенерировать новый ключ + + + +
+ Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки, + например, после переустановки ОС или чистки/смены браузера.
+ ПРЕДУПРЕЖДЕНИЕ! При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются + и шифруются ключом доступа перед отправкой на сервер. +
+
+
+
+
+ @@ -246,7 +345,7 @@ - +
Анимация
@@ -283,12 +382,13 @@
- + Включить управление кликом +