Эх сурвалжийг харах

Merge branch 'release/0.9.9'

Book Pauk 4 жил өмнө
parent
commit
094bb407ed

+ 11 - 6
client/components/ExternalLibs/ExternalLibs.vue

@@ -408,7 +408,7 @@ class ExternalLibs extends Vue {
     }
 
     get header() {
-        let result = (this.ready ? 'Библиотека' : 'Загрузка...');
+        let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
         if (this.ready && this.selectedLink) {
             result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
         }
@@ -787,12 +787,17 @@ class ExternalLibs extends Vue {
 
     showHelp() {
         this.$root.stdDialog.alert(`
-<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
-что особенно актуально для мобильных устройств.</p>
-
-<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
+<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
+что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
 на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
-к сожалению, в нем открываются не все страницы.
+к сожалению, в нем открываются не все страницы.</p>
+
+<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
+
+<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
+Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
+из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть 
+к третьим лицам.
 </p>
 
 <p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.

+ 3 - 0
client/components/Reader/ContentsPage/ContentsPage.vue

@@ -50,6 +50,9 @@
                     </div>
                 </div>
             </div>
+            <div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
+                Оглавление отсутствует
+            </div>
         </div>
     </div>
 

+ 2 - 2
client/components/Reader/Reader.vue

@@ -250,11 +250,11 @@ class Reader extends Vue {
                 await this.$nextTick();
                 this.paramPosIgnore = false;
             }
-        }, 500, {'maxWait':5000});
+        }, 500, {maxWait: 5000});
 
         this.scrollingSetRecentBook = _.debounce((newValue) => {
             this.debouncedSetRecentBook(newValue);
-        }, 15000, {'maxWait':20000});
+        }, 15000, {maxWait: 20000});
 
         document.addEventListener('fullscreenchange', () => {
             this.fullScreenActive = (document.fullscreenElement !== null);

+ 16 - 37
client/components/Reader/RecentBooksPage/RecentBooksPage.vue

@@ -59,7 +59,7 @@
                     <q-td key="links" :props="props" class="td-mp" auto-width>
                         <div class="break-word" style="width: 75px; font-size: 90%">
                             <a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
-                            <a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
+                            <a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
                         </div>
                     </q-td>
 
@@ -87,7 +87,7 @@
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import path from 'path';
-import _ from 'lodash';
+//import _ from 'lodash';
 
 import * as utils from '../../../share/utils';
 import Window from '../../share/Window.vue';
@@ -222,30 +222,11 @@ class RecentBooksPage extends Vue {
                 textLen = ` ${Math.round(book.textLength/1000)}k`;
             }
 
-            const fb2 = (book.fb2 ? book.fb2 : {});
-
-            let title = fb2.bookTitle;
-            if (title)
-                title = `"${title}"`;
-            else
-                title = '';
-
-            let author = '';
-            if (fb2.author) {
-                const authorNames = fb2.author.map(a => _.compact([
-                    a.lastName,
-                    a.firstName,
-                    a.middleName
-                ]).join(' '));
-                author = authorNames.join(', ');
-            } else {//TODO: убрать в будущем
-                author = _.compact([
-                    fb2.lastName,
-                    fb2.firstName,
-                    fb2.middleName
-                ]).join(' ');
-            }
-            author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
+            const bt = utils.getBookTitle(book.fb2);
+
+            let title = bt.bookTitle;
+            title = (title ? `"${title}"`: '');
+            const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
 
             result.push({
                 num,
@@ -256,9 +237,10 @@ class RecentBooksPage extends Vue {
                     author,
                     title: `${title}${perc}${textLen}`,
                 },
-                descString: `${author}${title}${perc}${textLen}`,
+                descString: `${author}${title}${perc}${textLen}`,//для сортировки
                 url: book.url,
                 path: book.path,
+                fullTitle: bt.fullTitle,
                 key: book.key,
             });
         }
@@ -291,13 +273,18 @@ class RecentBooksPage extends Vue {
         return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
     }
 
-    async downloadBook(fb2path) {
+    async downloadBook(fb2path, fullTitle) {
         try {
             await readerApi.checkCachedBook(fb2path);
 
             const d = this.$refs.download;
             d.href = fb2path;
-            d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
+            try {
+                const fn = utils.makeValidFilename(fullTitle);
+                d.download = fn.substring(0, 100) + '.fb2';
+            } catch(e) {
+                d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
+            }
 
             d.click();
         } catch (e) {
@@ -308,14 +295,6 @@ class RecentBooksPage extends Vue {
         }
     }
 
-    openOriginal(url) {
-        window.open(url, '_blank');
-    }
-
-    openFb2(path) {
-        window.open(path, '_blank');
-    }
-
     async handleDel(key) {
         await bookManager.delRecentBook({key});
         //this.updateTableData();//обновление уже происходит Reader.bookManagerEvent

+ 2 - 2
client/components/Reader/ServerStorage/ServerStorage.vue

@@ -507,10 +507,10 @@ class ServerStorage extends Vue {
                 if (md.key && result[md.key])
                     result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
 
-                if (!bookManager.loaded) {
+                /*if (!bookManager.loaded) {
                     this.warning('Ожидание загрузки списка книг перед синхронизацией');
                     while (!bookManager.loaded) await utils.sleep(100);
-                }
+                }*/
 
                 if (newRecent.rev != this.cachedRecent.rev)
                     await this.setCachedRecent(newRecent);

+ 18 - 31
client/components/Reader/TextPage/TextPage.vue

@@ -40,7 +40,7 @@ import Component from 'vue-class-component';
 import {loadCSS} from 'fg-loadcss';
 import _ from 'lodash';
 
-import {sleep} from '../../../share/utils';
+import * as utils from '../../../share/utils';
 import bookManager from '../share/bookManager';
 import DrawHelper from './DrawHelper';
 import rstore from '../../../store/modules/reader';
@@ -134,7 +134,7 @@ class TextPage extends Vue {
 
         this.$root.$on('resize', async() => {
             this.$nextTick(this.onResize);
-            await sleep(500);
+            await utils.sleep(500);
             this.$nextTick(this.onResize);
         });
     }
@@ -285,7 +285,7 @@ class TextPage extends Vue {
 
         let close = null;
         (async() => {
-            await sleep(500);
+            await utils.sleep(500);
             if (this.fontsLoading)
                 close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
         })();
@@ -342,7 +342,7 @@ class TextPage extends Vue {
             let i = 0;
             const t = this.parsed.testText;
             while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
-                await sleep(100);
+                await utils.sleep(100);
 
             if (this.parsed === parsed) {
                 this.parsed.testWidth = this.drawHelper.measureText(t, {});
@@ -366,7 +366,6 @@ class TextPage extends Vue {
         this.updateLayout();
         this.book = null;
         this.meta = null;
-        this.fb2 = null;
         this.parsed = null;
 
         this.linesUp = null;
@@ -383,7 +382,7 @@ class TextPage extends Vue {
                 try {
                     //подождем ленивый парсинг
                     this.stopLazyParse = true;
-                    while (this.doingLazyParse) await sleep(10);
+                    while (this.doingLazyParse) await utils.sleep(10);
 
                     const isParsed = await bookManager.hasBookParsed(this.lastBook);
                     if (!isParsed) {
@@ -392,21 +391,9 @@ class TextPage extends Vue {
 
                     this.book = await bookManager.getBook(this.lastBook);
                     this.meta = bookManager.metaOnly(this.book);
-                    this.fb2 = this.meta.fb2;
-
-                    let authorNames = [];
-                    if (this.fb2.author) {
-                        authorNames = this.fb2.author.map(a => _.compact([
-                            a.lastName,
-                            a.firstName,
-                            a.middleName
-                        ]).join(' '));
-                    }
+                    const bt = utils.getBookTitle(this.meta.fb2);
 
-                    this.title = _.compact([
-                        authorNames.join(', '),
-                        this.fb2.bookTitle
-                    ]).join(' - ');
+                    this.title = bt.fullTitle;
 
                     this.$root.$emit('set-app-title', this.title);
 
@@ -493,7 +480,7 @@ class TextPage extends Vue {
                 let wait = (timeout + 201)/100;
                 while (wait > 0 && !this[stopPropertyName]) {
                     wait--;
-                    await sleep(100);
+                    await utils.sleep(100);
                 }
                 resolve();
             })().catch(reject); });
@@ -509,7 +496,7 @@ class TextPage extends Vue {
         }
 
         //ждем анимацию
-        while (this.inAnimation) await sleep(10);
+        while (this.inAnimation) await utils.sleep(10);
 
         this.stopScrolling = false;
         this.doingScrolling = true;
@@ -520,7 +507,7 @@ class TextPage extends Vue {
             this.page1 = this.page2;
         this.toggleLayout = true;
         await this.$nextTick();
-        await sleep(50);
+        await utils.sleep(50);
 
         this.cachedPos = -1;
         this.draw();
@@ -557,7 +544,7 @@ class TextPage extends Vue {
         page.style.transform = 'none';
         page.offsetHeight;
 
-        while (this.doingScrolling) await sleep(10);
+        while (this.doingScrolling) await utils.sleep(10);
     }
 
     draw() {
@@ -766,7 +753,7 @@ class TextPage extends Vue {
         for (let i = 0; i < this.parsed.para.length; i++) {
             j++;
             if (j > 1) {
-                await sleep(1);
+                await utils.sleep(1);
                 j = 0;
             }
             if (this.stopLazyParse)
@@ -788,7 +775,7 @@ class TextPage extends Vue {
     async refreshTime() {
         if (!this.timeRefreshing) {
             this.timeRefreshing = true;
-            await sleep(60*1000);
+            await utils.sleep(60*1000);
 
             if (this.book && this.parsed.textLength) {
                 this.debouncedDrawStatusBar();
@@ -905,7 +892,7 @@ class TextPage extends Vue {
             this.settingsChanging = true;
             const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
             this.commit('reader/setSettings', {fontSize: newSize});
-            await sleep(50);
+            await utils.sleep(50);
             this.settingsChanging = false;
         }
     }
@@ -915,7 +902,7 @@ class TextPage extends Vue {
             this.settingsChanging = true;
             const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
             this.commit('reader/setSettings', {fontSize: newSize});
-            await sleep(50);
+            await utils.sleep(50);
             this.settingsChanging = false;
         }
     }
@@ -925,7 +912,7 @@ class TextPage extends Vue {
             this.settingsChanging = true;
             const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
             this.commit('reader/setSettings', {scrollingDelay: newDelay});
-            await sleep(50);
+            await utils.sleep(50);
             this.settingsChanging = false;
         }
     }
@@ -935,7 +922,7 @@ class TextPage extends Vue {
             this.settingsChanging = true;
             const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
             this.commit('reader/setSettings', {scrollingDelay: newDelay});
-            await sleep(50);
+            await utils.sleep(50);
             this.settingsChanging = false;
         }
     }
@@ -949,7 +936,7 @@ class TextPage extends Vue {
             let delay = 400;
             while (this.repDoing) {
                 this.handleClick(pointX, pointY);
-                await sleep(delay);
+                await utils.sleep(delay);
                 if (delay > 15)
                     delay *= 0.8;
             }

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

@@ -216,12 +216,32 @@ export default class BookParser {
                 }
             }
 
-            if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
+            if (path == '/fictionbook/description/title-info/author') {
                 if (!fb2.author)
                     fb2.author = [];
+
                 fb2.author.push({});
             }
 
+            const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
+            if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
+                if (!fb2.sequence)
+                    fb2.sequence = [];
+
+                if (!isPublishSequence || !fb2.sequence.length) {
+                    const attrs = sax.getAttrsSync(tail);
+                    const seq = {};
+                    if (attrs.name && attrs.name.value) {
+                        seq.name = attrs.name.value;
+                    }
+                    if (attrs.number && attrs.number.value) {
+                        seq.number = attrs.number.value;
+                    }
+
+                    fb2.sequence.push(seq);
+                }
+            }
+
             if (path.indexOf('/fictionbook/body') == 0) {
                 if (tag == 'body') {
                     if (isFirstBody && fb2.annotation) {
@@ -233,6 +253,14 @@ export default class BookParser {
                             newParagraph(' ', 1);
                     }
 
+                    if (isFirstBody && fb2.sequence && fb2.sequence.length) {
+                        const bt = utils.getBookTitle(fb2);
+                        if (bt.sequence) {
+                            newParagraph(bt.sequence, bt.sequence.length);
+                            newParagraph(' ', 1);
+                        }
+                    }
+
                     if (!isFirstBody)
                         newParagraph(' ', 1);
                     isFirstBody = false;

+ 131 - 82
client/components/Reader/share/bookManager.js

@@ -6,17 +6,23 @@ import BookParser from './BookParser';
 
 const maxDataSize = 300*1024*1024;//compressed bytes
 
+//локальный кэш метаданных книг, ограничение maxDataSize
 const bmMetaStore = localForage.createInstance({
     name: 'bmMetaStore'
 });
 
+//локальный кэш самих книг, ограничение maxDataSize
 const bmDataStore = localForage.createInstance({
     name: 'bmDataStore'
 });
 
-const bmRecentStore = localForage.createInstance({
+//список недавно открытых книг
+const bmRecentStoreOld = localForage.createInstance({
     name: 'bmRecentStore'
 });
+const bmRecentStoreNew = localForage.createInstance({
+    name: 'bmRecentStoreNew'
+});
 
 class BookManager {
     async init(settings) {
@@ -25,15 +31,74 @@ class BookManager {
 
         this.eventListeners = [];
         this.books = {};
+
         this.recent = {};
+        this.saveRecent = _.debounce(() => {
+            bmRecentStoreNew.setItem('recent', this.recent);
+        }, 300, {maxWait: 800});
+
+        this.saveRecentItem = _.debounce(() => {
+            bmRecentStoreNew.setItem('recent-item', this.recentItem);
+            this.recentRev++;
+            bmRecentStoreNew.setItem('rev', this.recentRev);
+        }, 200, {maxWait: 300});
+
+        //загрузка bmRecentStore
+        this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
+        if (this.recentRev) {
+            this.recent = await bmRecentStoreNew.getItem('recent');
+            if (!this.recent)
+                this.recent = {};
+
+            this.recentItem = await bmRecentStoreNew.getItem('recent-item');
+            if (this.recentItem)
+                this.recent[this.recentItem.key] = this.recentItem;
+
+            this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
+            if (this.recentLastKey) {
+                const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
+                if (_.isObject(meta)) {
+                    this.books[meta.key] = meta;
+                }
+            }
+
+            await this.cleanRecentBooks();
 
-        this.recentLast = await bmRecentStore.getItem('recent-last');
-        if (this.recentLast) {
-            this.recent[this.recentLast.key] = this.recentLast;
-            const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
-            if (_.isObject(meta)) {
-                this.books[meta.key] = meta;
+        } else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
+            this.recentLast = await bmRecentStoreOld.getItem('recent-last');
+            if (this.recentLast) {
+                this.recent[this.recentLast.key] = this.recentLast;
+                const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
+                if (_.isObject(meta)) {
+                    this.books[meta.key] = meta;
+                }
+            }
+
+            let key = null;
+            const len = await bmRecentStoreOld.length();
+            for (let i = len - 1; i >= 0; i--) {
+                key = await bmRecentStoreOld.key(i);
+                if (key) {
+                    let r = await bmRecentStoreOld.getItem(key);
+                    if (_.isObject(r) && r.key) {
+                        this.recent[r.key] = r;
+                    }
+                } else  {
+                    await bmRecentStoreOld.removeItem(key);
+                }
             }
+
+            //размножение для дебага
+            /*if (key) {
+                for (let i = 0; i < 1000; i++) {
+                    const k = this.keyFromUrl(i.toString());
+                    this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
+                }
+            }*/
+
+            await bmRecentStoreNew.setItem('recent', this.recent);
+            this.recentRev = 1;
+            await bmRecentStoreNew.setItem('rev', this.recentRev);
         }
 
         this.recentChanged = true;
@@ -41,9 +106,7 @@ class BookManager {
         this.loadStored();//no await
     }
 
-    //Долгая асинхронная загрузка из хранилища.
-    //Хранение в отдельных записях дает относительно
-    //нормальное поведение при нескольких вкладках с читалкой в браузере.
+    //Ленивая асинхронная загрузка bmMetaStore
     async loadStored() {
         //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
         await utils.sleep(2000);
@@ -70,32 +133,7 @@ class BookManager {
             }
         }
 
-        let key = null;
-        len = await bmRecentStore.length();
-        for (let i = len - 1; i >= 0; i--) {
-            key = await bmRecentStore.key(i);
-            if (key) {
-                let r = await bmRecentStore.getItem(key);
-                if (_.isObject(r) && r.key) {
-                    this.recent[r.key] = r;
-                }
-            } else  {
-                await bmRecentStore.removeItem(key);
-            }
-        }
-
-        //размножение для дебага
-        /*if (key) {
-            for (let i = 0; i < 1000; i++) {
-                const k = this.keyFromUrl(i.toString());
-                this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
-            }
-        }*/
-        
         await this.cleanBooks();
-        await this.cleanRecentBooks();
-
-        this.recentChanged = true;
         this.loaded = true;
         this.emit('load-stored-finish');
     }
@@ -238,7 +276,7 @@ class BookManager {
         let book = this.books[meta.key];
 
         if (!book && !this.loaded) {
-            book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
+            book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
             if (book)
                 this.books[meta.key] = book;
         }
@@ -254,7 +292,7 @@ class BookManager {
         result = this.books[meta.key];
 
         if (!result) {
-            result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
+            result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
             if (result)
                 this.books[meta.key] = result;
         }
@@ -328,6 +366,43 @@ class BookManager {
     }
 
     //-- recent --------------------------------------------------------------
+    async recentSetItem(item = null, skipCheck = false) {
+        const rev = await bmRecentStoreNew.getItem('rev');
+        if (rev != this.recentRev && !skipCheck) {
+            const newRecent = await bmRecentStoreNew.getItem('recent');
+            Object.assign(this.recent, newRecent);
+            this.recentItem = await bmRecentStoreNew.getItem('recent-item');
+            this.recentRev = rev;
+        }
+
+        const prevKey = (this.recentItem ? this.recentItem.key : '');
+        if (item) {
+            this.recent[item.key] = item;
+            this.recentItem = item;
+        } else {
+            this.recentItem = null;
+        }
+
+        this.saveRecentItem();
+
+        if (!item || prevKey != item.key) {
+            this.saveRecent();
+        }
+
+        this.recentChanged = true;
+
+        if (item) {
+            this.emit('recent-changed', item.key);
+        } else {
+            this.emit('recent-changed');
+        }
+    }
+
+    async recentSetLastKey(key) {
+        this.recentLastKey = key;
+        await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
+    }
+
     async setRecentBook(value) {
         const result = this.metaOnly(value);
         result.touchTime = Date.now();
@@ -341,38 +416,25 @@ class BookManager {
                 result.bookPosSeen = this.recent[result.key].bookPosSeen;
         }
 
-        this.recent[result.key] = result;
-
-        await bmRecentStore.setItem(result.key, result);
-
-        this.recentLast = result;
-        await bmRecentStore.setItem('recent-last', this.recentLast);
-
-        this.recentChanged = true;
-        this.emit('recent-changed', result.key);
+        await this.recentSetLastKey(result.key);
+        await this.recentSetItem(result);
         return result;
     }
 
     async getRecentBook(value) {
-        let result = this.recent[value.key];
-        if (!result) {
-            result = await bmRecentStore.getItem(value.key);
-            if (result)
-                this.recent[value.key] = result;
-        }
-        return result;
+        return this.recent[value.key];
     }
 
     async delRecentBook(value) {
-        this.recent[value.key].deleted = 1;
-        await bmRecentStore.setItem(value.key, this.recent[value.key]);
+        const item = this.recent[value.key];
+        item.deleted = 1;
 
-        if (this.recentLast.key == value.key) {
-            this.recentLast = null;
-            await bmRecentStore.setItem('recent-last', this.recentLast);
+        if (this.recentLastKey == value.key) {
+            await this.recentSetLastKey(null);
         }
+
+        await this.recentSetItem(item);
         this.emit('recent-deleted', value.key);
-        this.emit('recent-changed', value.key);
     }
 
     async cleanRecentBooks() {
@@ -380,24 +442,22 @@ class BookManager {
 
         let isDel = false;
         for (let i = 1000; i < sorted.length; i++) {
-            await bmRecentStore.removeItem(sorted[i].key);
             delete this.recent[sorted[i].key];
-            await bmRecentStore.removeItem(sorted[i].key);
             isDel = true;
         }
 
         this.sortedRecentCached = null;
 
         if (isDel)
-            this.emit('recent-changed');
+            await this.recentSetItem();
         return isDel;
     }
 
     mostRecentBook() {
-        if (this.recentLast) {
-            return this.recentLast;
+        if (this.recentLastKey) {
+            return this.recent[this.recentLastKey];
         }
-        const oldRecentLast = this.recentLast;
+        const oldKey = this.recentLastKey;
 
         let max = 0;
         let result = null;
@@ -408,10 +468,11 @@ class BookManager {
                 result = book;
             }
         }
-        this.recentLast = result;
-        bmRecentStore.setItem('recent-last', this.recentLast);//no await
+        
+        const newRecentLastKey = (result ? result.key : null);
+        this.recentSetLastKey(newRecentLastKey);//no await
 
-        if (this.recentLast !== oldRecentLast)
+        if (newRecentLastKey !== oldKey)
             this.emit('recent-changed');
 
         return result;
@@ -442,24 +503,12 @@ class BookManager {
                 delete mergedRecent[i];
         }
         
-        //"ленивое" обновление хранилища
-        (async() => {
-            for (const rec of Object.values(mergedRecent)) {
-                if (rec.key) {
-                    await bmRecentStore.setItem(rec.key, rec);
-                    await utils.sleep(1);
-                }
-            }
-        })();
-
         this.recent = mergedRecent;
 
-        this.recentLast = null;
-        await bmRecentStore.setItem('recent-last', this.recentLast);
+        await this.recentSetLastKey(null);
+        await this.recentSetItem(null, true);
 
-        this.recentChanged = true;
         this.emit('set-recent');
-        this.emit('recent-changed');
     }
 
     addEventListener(listener) {

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

@@ -1,4 +1,15 @@
 export const versionHistory = [
+{
+    showUntil: '2020-11-20',
+    header: '0.9.9 (2020-11-21)',
+    content:
+`
+<ul>
+    <li>оптимизации, исправления багов</li>
+</ul>
+`
+},
+
 {
     showUntil: '2020-11-12',
     header: '0.9.8 (2020-11-13)',

+ 48 - 0
client/share/utils.js

@@ -308,3 +308,51 @@ export function userHotKeysObjectSwap(userHotKeys) {
 export function removeHtmlTags(s) {
     return s.replace(/(<([^>]+)>)/ig, '');
 }
+
+export function makeValidFilename(filename, repl = '_') {
+    let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
+    f = f.trim();
+    while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
+        f = f.substring(0, f.length - 1);
+    }
+
+    if (f)
+        return f;
+    else
+        throw new Error('Invalid filename');
+}
+
+export function getBookTitle(fb2) {
+    fb2 = (fb2 ? fb2 : {});
+    const result = {};
+
+   if (fb2.author) {
+        const authorNames = fb2.author.map(a => _.compact([
+            a.lastName,
+            a.firstName,
+            a.middleName
+        ]).join(' '));
+
+        result.author = authorNames.join(', ');
+    }
+
+    if (fb2.sequence) {
+        const seqs = fb2.sequence.map(s => _.compact([
+            s.name,
+            (s.number ? `#${s.number}` : null),
+        ]).join(' '));
+
+        result.sequence = seqs.join(', ');
+        if (result.sequence)
+            result.sequenceTitle = `(${result.sequence})`;
+    }
+
+    result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
+
+    result.fullTitle = _.compact([
+        result.author,
+        result.bookTitle
+    ]).join(' - ');
+
+    return result;
+}

+ 1 - 1
client/store/modules/reader.js

@@ -16,7 +16,7 @@ const readerActions = {
     'refresh': 'Принудительно обновить книгу',
     'offlineMode': 'Автономный режим (без интернета)',
     'contents': 'Оглавление/закладки',
-    'libs': 'Библиотека',
+    'libs': 'Сетевая библиотека',
     'recentBooks': 'Открыть недавние',
     'switchToolbar': 'Показать/скрыть панель управления',
     'donate': '',

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.9.8",
+  "version": "0.9.9",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
package.json

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

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

@@ -171,6 +171,7 @@ class ReaderWorker {
 
         } catch (e) {
             log(LM_ERR, e.stack);
+            log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
             if (e.message == 'abort')
                 e.message = overLoadMes;
             wState.set({state: 'error', error: e.message});