Browse Source

Merge branch 'release/0.9.4'

Book Pauk 4 năm trước cách đây
mục cha
commit
cf9ce26438
64 tập tin đã thay đổi với 1219 bổ sung237 xóa
  1. 1 1
      README.md
  2. 22 9
      client/api/webSocketConnection.js
  3. 47 69
      client/components/App.vue
  4. 535 0
      client/components/ExternalLibs/ExternalLibs.vue
  5. 1 1
      client/components/Reader/CopyTextPage/CopyTextPage.vue
  6. 8 4
      client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue
  7. 1 1
      client/components/Reader/HelpPage/HelpPage.vue
  8. 131 0
      client/components/Reader/LibsPage/LibsPage.vue
  9. 7 5
      client/components/Reader/LoaderPage/LoaderPage.vue
  10. 1 1
      client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue
  11. 73 44
      client/components/Reader/Reader.vue
  12. 1 1
      client/components/Reader/RecentBooksPage/RecentBooksPage.vue
  13. 1 1
      client/components/Reader/SearchPage/SearchPage.vue
  14. 92 2
      client/components/Reader/ServerStorage/ServerStorage.vue
  15. 1 1
      client/components/Reader/SetPositionPage/SetPositionPage.vue
  16. 1 1
      client/components/Reader/SettingsPage/SettingsPage.vue
  17. 4 2
      client/components/Reader/SettingsPage/include/ButtonsTab.inc
  18. 1 1
      client/components/Reader/SettingsPage/include/ProfilesTab.inc
  19. 1 1
      client/components/Reader/TextPage/TextPage.vue
  20. 10 5
      client/components/Reader/share/BookParser.js
  21. 13 0
      client/components/Reader/versionHistory.js
  22. 10 9
      client/components/share/Notify.vue
  23. 2 2
      client/components/share/StdDialog.vue
  24. 1 0
      client/components/share/Window.vue
  25. 0 1
      client/index.html.template
  26. 20 21
      client/router.js
  27. 36 1
      client/store/modules/reader.js
  28. 0 0
      docs/beta.omnireader.ru/beta.omnireader
  29. 0 0
      docs/beta.omnireader.ru/deploy.sh
  30. 3 0
      docs/beta.omnireader.ru/run_server.sh
  31. 0 11
      docs/beta.omnireader/run_server.sh
  32. 128 0
      docs/liberama.top/liberama
  33. 12 6
      docs/omnireader.ru/README.md
  34. 8 0
      docs/omnireader.ru/cron_server.sh
  35. 0 0
      docs/omnireader.ru/deploy.sh
  36. 0 0
      docs/omnireader.ru/old/.htaccess
  37. 0 0
      docs/omnireader.ru/old/apple-touch-icon-precomposed.png
  38. 0 0
      docs/omnireader.ru/old/apple-touch-icon.png
  39. 0 0
      docs/omnireader.ru/old/config/config.js
  40. 0 0
      docs/omnireader.ru/old/config/config.php
  41. 0 0
      docs/omnireader.ru/old/f.php
  42. 0 0
      docs/omnireader.ru/old/favicon.ico
  43. 0 0
      docs/omnireader.ru/old/index.html
  44. 0 0
      docs/omnireader.ru/old/info.txt
  45. 0 0
      docs/omnireader.ru/old/js/bpr319.js
  46. 0 0
      docs/omnireader.ru/old/js/bpricon.gif
  47. 0 0
      docs/omnireader.ru/old/js/colo58.png
  48. 0 0
      docs/omnireader.ru/old/js/load.gif
  49. 0 0
      docs/omnireader.ru/old/js/stylex.css
  50. 0 0
      docs/omnireader.ru/old/parser.php
  51. 0 0
      docs/omnireader.ru/old/robots.txt
  52. 0 0
      docs/omnireader.ru/old/test.php
  53. 0 0
      docs/omnireader.ru/old/txt/.htaccess
  54. 0 0
      docs/omnireader.ru/omnireader
  55. 0 0
      docs/omnireader.ru/omnireader_http
  56. 4 0
      docs/omnireader.ru/start_server.sh
  57. 4 0
      docs/omnireader.ru/stop_server.sh
  58. 0 11
      docs/omnireader/run_server.sh
  59. 4 4
      package-lock.json
  60. 2 2
      package.json
  61. 7 2
      server/core/FileDecompressor.js
  62. 1 1
      server/core/Zip/node_stream_zip.js
  63. 16 16
      server/core/sax.js
  64. 9 0
      server/core/utils.js

+ 1 - 1
README.md

@@ -8,7 +8,7 @@
 ![](docs/assets/reader.jpg)
 
 ## VPS
-Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
+Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
 
 ## Сборка проекта
 Необходима версия node.js не ниже 10.

+ 22 - 9
client/api/webSocketConnection.js

@@ -1,3 +1,5 @@
+import * as utils from '../share/utils';
+
 const cleanPeriod = 60*1000;//1 минута
 
 class WebSocketConnection {
@@ -9,6 +11,8 @@ class WebSocketConnection {
         this.messageQueue = [];
         this.messageLifeTime = messageLifeTime;
         this.requestId = 0;
+
+        this.connecting = false;
     }
 
     addListener(listener) {
@@ -53,14 +57,22 @@ class WebSocketConnection {
     }
 
     open(url) {
-        return new Promise((resolve, reject) => {
+        return new Promise((resolve, reject) => { (async() => {
+            //Ожидаем окончания процесса подключения, если open уже был вызван
+            let i = 0;
+            while (this.connecting && i < 200) {//10 сек
+                await utils.sleep(50);
+                i++;
+            }
+            if (i >= 200)
+                this.connecting = false;
+
+            //проверим подключение, и если нет, то подключимся заново
             if (this.ws && this.ws.readyState == WebSocket.OPEN) {
                 resolve(this.ws);
             } else {
-                let protocol = 'ws:';
-                if (window.location.protocol == 'https:') {
-                    protocol = 'wss:'
-                }
+                this.connecting = true;
+                const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
 
                 url = url || `${protocol}//${window.location.host}/ws`;
                 
@@ -71,9 +83,8 @@ class WebSocketConnection {
                 }
                 this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
 
-                let resolved = false;
                 this.ws.onopen = (e) => {
-                    resolved = true;
+                    this.connecting = false;
                     resolve(e);
                 };
 
@@ -97,11 +108,13 @@ class WebSocketConnection {
 
                 this.ws.onerror = (e) => {
                     this.emit(e.message, true);
-                    if (!resolved)
+                    if (this.connecting) {
+                        this.connecting = false;
                         reject(e);
+                    }
                 };
             }
-        });
+        })() });
     }
 
     //timeout в минутах (cleanPeriod)

+ 47 - 69
client/components/App.vue

@@ -1,56 +1,4 @@
 <template>
-    <!--q-layout view="lhr lpr lfr">
-        <q-drawer v-model="showAsideBar" :width="asideWidth">
-            <div class="app-name"><span v-html="appName"></span></div>
-            <q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
-
-            <q-list>
-                <q-item clickable v-ripple>
-                    <q-item-section avatar>
-                        <q-icon name="inbox" />
-                    </q-item-section>
-
-                    <q-item-section>Inbox</q-item-section>
-                </q-item>
-            </q-list-->
-            <!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
-              <el-menu-item index="/cardindex">
-                <i class="el-icon-search"></i>
-                <span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/reader">
-                <i class="el-icon-tickets"></i>
-                <span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/forum" disabled>
-                <i class="el-icon-message"></i>
-                <span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/income">
-                <i class="el-icon-upload"></i>
-                <span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/sources">
-                <i class="el-icon-menu"></i>
-                <span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/settings">
-                <i class="el-icon-setting"></i>
-                <span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
-              </el-menu-item>
-              <el-menu-item index="/help">
-                <i class="el-icon-question"></i>
-                <span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
-              </el-menu-item>
-            </el-menu-->
-        <!--/q-drawer>
-
-        <q-page-container>
-            <keep-alive>
-                <router-view></router-view>
-            </keep-alive>
-        </q-page-container>
-    </q-layout-->
     <div class="fit row">
         <Notify ref="notify"/>
         <StdDialog ref="stdDialog"/>
@@ -90,7 +38,8 @@ class App extends Vue {
         '/sources': 'Источники',
         '/settings': 'Параметры',
         '/help': 'Справка',
-    }
+    };
+
     created() {
         this.commit = this.$store.commit;
         this.dispatch = this.$store.dispatch;
@@ -106,10 +55,20 @@ class App extends Vue {
                 cachedPath = this.$route.path;
                 const m = cachedPath.match(/^(\/[^/]*).*$/i);
                 cachedRoute = (m ? m[1] : this.$route.path);
+
             }
             return cachedRoute;
         }
 
+        this.$router.beforeEach((to, from, next) => {
+            //распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
+            if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
+                next('/404');
+            } else {
+                next();
+            }
+        });
+
         // set-app-title
         this.$root.$on('set-app-title', this.setAppTitle);
 
@@ -133,15 +92,26 @@ class App extends Vue {
 
         document.addEventListener('keyup', (event) => {
             this.keyHook(event);
-        });        
+        });
+        document.addEventListener('keypress', (event) => {
+            this.keyHook(event);
+        });
         document.addEventListener('keydown', (event) => {
             this.keyHook(event);
-        });        
+        });
         window.addEventListener('resize', () => {
             this.$root.$emit('resize');
         });
     }
 
+    routerReady() {
+        return new Promise ((resolve) => {
+            this.$router.onReady(() => {
+                resolve();
+            });
+        });
+    }
+
     mounted() {
         this.$root.notify = this.$refs.notify;
         this.$root.stdDialog = this.$refs.stdDialog;
@@ -157,7 +127,10 @@ class App extends Vue {
         });
 
         this.setAppTitle();
-        this.redirectIfNeeded();
+        (async() => {
+            await this.routerReady();
+            this.redirectIfNeeded();
+        })();
     }
 
     toggleCollapse() {
@@ -202,7 +175,9 @@ class App extends Vue {
 
     setAppTitle(title) {
         if (!title) {
-            if (this.mode == 'omnireader') {
+            if (this.mode == 'liberama.top') {
+                document.title = `Liberama Reader - всегда с вами`;
+            } else if (this.mode == 'omnireader') {
                 document.title = `Omni Reader - всегда с вами`;
             } else if (this.config && this.mode !== null) {
                 document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
@@ -221,29 +196,32 @@ class App extends Vue {
     }
 
     get showAsideBar() {
-        return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
+        return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
     }
 
     set showAsideBar(value) {
     }
 
     get isReaderActive() {
-        return this.rootRoute == '/reader';
+        return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
     }
 
     redirectIfNeeded() {
-        if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
-            //старый url
+        if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
             const search = window.location.search.substr(1);
-            const s = search.split('url=');
-            const url = s[1] || '';
-            const q = utils.parseQuery(s[0] || '');
-            if (url) {
-                q.url = decodeURIComponent(url);
-            }
 
-            window.history.replaceState({}, '', '/');
-            this.$router.replace({ path: '/reader', query: q });
+            //распознавание параметра url вида "?url=<link>" и редирект при необходимости
+            if (!this.isReaderActive) {
+                const s = search.split('url=');
+                const url = s[1] || '';
+                const q = utils.parseQuery(s[0] || '');
+                if (url) {
+                    q.url = decodeURIComponent(url);
+                }
+
+                window.history.replaceState({}, '', '/');
+                this.$router.replace({ path: '/reader', query: q });
+            }
         }
     }
 }

+ 535 - 0
client/components/ExternalLibs/ExternalLibs.vue

@@ -0,0 +1,535 @@
+<template>
+    <Window ref="window" @close="close">
+        <template slot="header">
+            {{ header }}
+        </template>
+
+        <template slot="buttons">
+            <span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
+                <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
+                <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
+            </span>
+        </template>
+
+        <div v-show="ready" class="col column" style="min-width: 600px">
+            <div class="row items-center q-px-sm" style="height: 50px">
+                <q-select class="q-mr-sm" v-model="rootLink" :options="rootLinkOptions"
+                    style="width: 230px"
+                    dropdown-icon="la la-angle-down la-sm"
+                    rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
+                >
+                    <template v-slot:prepend>
+                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
+                        </q-btn>
+                        <q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings"  size="12px" disabled>
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки (пока недоступно)</q-tooltip>                            
+                        </q-btn>
+                    </template>
+                    <template v-slot:selected>
+                        <div style="overflow: hidden; white-space: nowrap;">{{ removeProtocol(rootLink) }}</div>
+                    </template>
+                </q-select>
+                <q-select class="q-mr-sm" v-model="selectedLink" :options="selectedLinkOptions" style="width: 50px"
+                    dropdown-icon="la la-angle-down la-sm"
+                    rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
+                >
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip>
+                </q-select>
+                <q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги" @focus="onInputFocus">
+                    <template v-slot:prepend>
+                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(libs.startLink)" size="12px">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
+                        </q-btn>
+                        <q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
+                        </q-btn>
+                    </template>
+                </q-input>
+                <q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl">Открыть
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
+                </q-btn>
+            </div>
+            <div class="separator"></div>
+
+            <iframe v-if="frameVisible" class="col fit" ref="frame" :src="frameSrc" frameborder="0"></iframe>
+
+            <Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
+                <template slot="header">
+                    <div class="row items-center">
+                        <q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
+                        Добавить закладку
+                    </div>
+                </template>
+
+                <div class="q-mx-md row">
+                    <q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" 
+                        placeholder="Ссылка для закладки" maxlength="2000" @focus="onInputFocus">
+                    </q-input>
+
+                    <q-select class="q-mr-sm" v-model="defaultRootLink" :options="defaultRootLinkOptions" style="width: 50px"
+                        dropdown-icon="la la-angle-down la-sm"
+                        outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
+                    >
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip>
+                    </q-select>
+                </div>
+
+                <div class="q-mx-md q-mt-md">
+                    <q-input class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkDesc" 
+                        placeholder="Описание" style="width: 400px" maxlength="100" @focus="onInputFocus">
+                    </q-input>
+                </div>
+
+                <template slot="footer">
+                    <q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
+                    <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
+                </template>
+            </Dialog>
+        </div>
+    </Window>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+import _ from 'lodash';
+
+import Window from '../share/Window.vue';
+import Dialog from '../share/Dialog.vue';
+import rstore from '../../store/modules/reader';
+import * as utils from '../../share/utils';
+
+const proxySubst = {
+    'http://flibusta.is': 'http://b.liberama.top:23480',
+};
+
+export default @Component({
+    components: {
+        Window,
+        Dialog
+    },
+    watch: {
+        libs: function() {
+            this.loadLibs();
+        },
+        rootLink: function() {
+            this.updateSelectedLink();
+            this.updateStartLink();
+        },
+        selectedLink: function() {
+            this.updateStartLink();
+        },
+        defaultRootLink: function() {
+            this.updateBookmarkLink();
+        }
+    }    
+})
+class ExternalLibs extends Vue {
+    ready = false;
+    frameVisible = false;
+    startLink = '';
+    rootLink = '';
+    selectedLink = '';
+    frameSrc = '';
+    bookUrl = '';
+    libs = {};
+    fullScreenActive = false;
+    addBookmarkVisible = false;
+
+    bookmarkLink = '';
+    bookmarkDesc = '';
+    defaultRootLink = '';
+
+    created() {
+        this.$root.addKeyHook(this.keyHook);
+
+        //this.commit = this.$store.commit;
+        //this.commit('reader/setLibs', rstore.libsDefaults);
+    }
+
+    mounted() {
+        (async() => {
+            //подождем this.mode
+            let i = 0;
+            while(!this.mode && i < 100) {
+                await utils.sleep(100);
+                i++;
+            }
+
+            if (this.mode != 'liberama.top') {
+                this.$router.replace('/404');
+                return;
+            }
+
+            this.$refs.window.init();
+
+            this.opener = null;
+            const host = window.location.host;
+            const openerHost = (host.indexOf('b.') == 0 ? host.substring(2) : host);
+            const openerOrigin1 = `http://${openerHost}`;
+            const openerOrigin2 = `https://${openerHost}`;
+
+            window.addEventListener('message', (event) => {
+                if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
+                    return;
+                if (!_.isObject(event.data) || event.data.from != 'LibsPage')
+                    return;
+                if (event.origin == openerOrigin1)
+                    this.opener = window.opener;
+                else
+                    this.opener = event.source;
+                this.openerOrigin = event.origin;
+
+                //console.log(event);
+
+                this.recvMessage(event.data);
+            });
+
+            //Ожидаем родителя
+            i = 0;
+            while(!this.opener) {
+                await utils.sleep(1000);
+                i++;
+                if (i >= 5) {
+                    await this.$root.stdDialog.alert('Нет связи с читалкой. Окно будет закрыто', 'Ошибка');
+                    window.close();
+                }
+            }
+
+            //Проверка закрытия родительского окна
+            while(this.opener) {
+                await this.checkOpener();
+                await utils.sleep(1000);
+            }
+        })();
+    }
+
+    recvMessage(d) {
+        if (d.type == 'mes') {
+            switch(d.data) {
+                case 'hello': this.sendMessage({type: 'mes', data: 'ready'}); break;
+            }
+        } else if (d.type == 'libs') {
+            this.ready = true;
+            this.libs = _.cloneDeep(d.data);
+            if (!this.frameSrc)
+                this.goToLink(this.libs.startLink);
+        } else if (d.type == 'notify') {
+            this.$root.notify.success(d.data, '', {position: 'bottom-right'});
+        }
+    }
+
+    sendMessage(d) {
+        (async() => {
+            await this.checkOpener();
+            if (this.opener && this.openerOrigin)
+                this.opener.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.openerOrigin);
+        })();
+    }
+
+    async checkOpener() {
+        if (this.opener.closed) {
+            await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
+            window.close();
+        }
+    }
+
+    commitLibs(libs) {
+        this.sendMessage({type: 'libs', data: libs});
+    }
+
+    loadLibs() {
+        const libs = this.libs;
+        this.startLink = (libs.comment ? libs.comment + ' ': '') + this.removeProtocol(libs.startLink);
+        this.rootLink = this.getOrigin(libs.startLink);
+        this.updateSelectedLink();
+    }
+
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+
+    get header() {
+        let result = (this.ready ? 'Библиотека' : 'Загрузка...');
+        if (this.ready && this.startLink) {
+            result += ` | ${this.startLink}`;
+        }
+        this.$root.$emit('set-app-title', result);
+        return result;
+    }
+
+    updateSelectedLink() {
+        if (!this.ready)
+            return;
+        const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
+        if (index >= 0)
+            this.selectedLink = this.libs.groups[index].s;
+    }
+
+    updateStartLink() {
+        if (!this.ready)
+            return;
+        const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
+        if (index >= 0) {
+            let libs = _.cloneDeep(this.libs);
+            libs.groups[index].s = this.selectedLink;
+            libs.startLink = this.selectedLink;
+            libs.comment = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
+            this.goToLink(this.selectedLink);
+            this.commitLibs(libs);
+        }
+    }
+
+    get rootLinkOptions() {
+        let result = [];
+        if (!this.ready)
+            return result;
+
+        this.libs.groups.forEach(group => {
+            result.push({label: this.removeProtocol(group.r), value: group.r});
+        });
+
+        return result;
+    }
+
+    get defaultRootLinkOptions() {
+        let result = [];
+
+        rstore.libsDefaults.groups.forEach(group => {
+            result.push({label: this.removeProtocol(group.r), value: group.r});
+        });
+
+        return result;
+    }
+
+    get selectedLinkOptions() {
+        let result = [];
+        if (!this.ready)
+            return result;
+
+        const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
+        if (index >= 0) {
+            this.libs.groups[index].list.forEach(link => {
+                result.push({label: (link.c ? link.c + ' ': '') + this.removeOrigin(link.l), value: link.l});
+            });
+        }
+
+        return result;
+    }
+
+    openBookUrlInFrame() {
+        if (this.bookUrl)
+            this.goToLink(this.addProtocol(this.bookUrl));
+    }
+
+    goToLink(link) {
+        if (!this.ready)
+            return;
+
+        this.frameSrc = this.makeProxySubst(link);
+        this.frameVisible = false;
+        this.$nextTick(() => {
+            this.frameVisible = true;
+        });
+    }
+
+    addProtocol(url) {
+        if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
+            return 'http://' + url;
+        return url;
+    }
+
+    removeProtocol(url) {
+        return url.replace(/(^\w+:|^)\/\//, '');
+    }
+
+    getOrigin(url) {
+        const parsed = new URL(url);
+        return parsed.origin;
+    }
+
+    removeOrigin(url) {
+        const parsed = new URL(url);
+        const result = url.substring(parsed.origin.length);
+        return (result ? result : '/');
+    }
+
+    getRootIndexByUrl(groups, url) {
+        if (!this.ready)
+            return -1;
+        const origin = this.getOrigin(url);
+        for (let i = 0; i < groups.length; i++) {
+            if (groups[i].r == origin)
+                return i;
+        }
+        return -1;
+    }
+
+    getListItemByLink(list, link) {
+        for (const item of list) {
+            if (item.l == link)
+                return item;
+        }
+        return null;
+    }
+
+    getCommentByLink(list, link) {
+        const item = this.getListItemByLink(list, link);
+        return (item ? item.c : '');
+    }
+
+    makeProxySubst(url, reverse = false) {
+        for (const [key, value] of Object.entries(proxySubst)) {
+            if (reverse && value == url.substring(0, value.length)) {
+                return key + url.substring(value.length);
+            } else if (key == url.substring(0, key.length)) {
+                return value + url.substring(key.length);
+            }
+        }
+
+        return url;
+    }
+
+    onInputFocus(event) {
+        if (event.target.select)
+            event.target.select();
+    }
+
+    submitUrl() {
+        if (this.bookUrl) {
+            this.sendMessage({type: 'submitUrl', data: {
+                url: this.makeProxySubst(this.addProtocol(this.bookUrl), true), 
+                force: true
+            }});
+            this.bookUrl = '';
+            if (this.libs.closeAfterSubmit)
+                this.close();
+        }
+    }
+
+    addBookmark() {
+        this.bookmarkLink = (this.bookUrl ? this.makeProxySubst(this.addProtocol(this.bookUrl), true) : '');
+        this.bookmarkDesc = '';
+        this.addBookmarkVisible = true;
+        this.$nextTick(() => {
+            this.$refs.bookmarkLink.focus();
+        });
+    }
+
+    updateBookmarkLink() {
+        const index = this.getRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
+        if (index >= 0) {
+            this.bookmarkLink = rstore.libsDefaults.groups[index].s;
+            this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
+        } else {
+            this.bookmarkLink = '';
+            this.bookmarkDesc = '';
+        }
+    }
+
+    async okAddBookmark() {
+        const link = this.addProtocol(this.bookmarkLink);
+        let index = -1;
+        try {
+            index = this.getRootIndexByUrl(this.libs.groups, link);
+        } catch (e) {
+            await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
+            return;
+        }
+
+        //есть группа в закладках
+        if (index >= 0) {
+            const item = this.getListItemByLink(this.libs.groups[index].list, link);
+            
+            if (!item || item.c != this.bookmarkDesc) {
+                //добавляем
+                let libs = _.cloneDeep(this.libs);
+
+                if (libs.groups[index].list.length >= 100) {
+                    await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
+                    return;
+                }
+
+                libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
+                this.commitLibs(libs);
+            }
+        } else {//нет группы в закладках
+            let libs = _.cloneDeep(this.libs);
+
+            if (libs.groups.length >= 100) {
+                await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
+                return;
+            }
+
+            //добавляем сначала группу
+            libs.groups.push({r: this.getOrigin(link), s: link, list: []});
+            
+            index = this.getRootIndexByUrl(libs.groups, link);
+            if (index >= 0)
+                libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
+
+            this.commitLibs(libs);
+        }
+
+        this.addBookmarkVisible = false;
+    }
+
+    bookmarkSettings() {
+    }
+
+    fullScreenToggle() {
+        this.fullScreenActive = !this.fullScreenActive;
+        if (this.fullScreenActive) {
+            this.$q.fullscreen.request();
+        } else {
+            this.$q.fullscreen.exit();
+        }
+    }
+
+    close() {
+        this.sendMessage({type: 'close'});
+    }
+
+    keyHook() {
+        if (this.$root.rootRoute() == '/external-libs') {
+            if (this.$refs.dialogAddBookmark.active)
+                return false;
+
+            //недостатки сторонних ui
+            const input = this.$refs.input.$refs.input;
+            if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
+                this.submitUrl();
+                return true;
+            }
+
+            if (event.type == 'keydown' && event.key == 'Escape') {
+                this.close();
+            }
+            return true;
+        }
+        return false;
+    }
+
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.separator {
+    height: 1px;
+    background-color: #A0A0A0;
+}
+
+.full-screen-button {
+    width: 30px;
+    height: 30px;
+    cursor: pointer;
+}
+
+.full-screen-button:hover {
+    background-color: #69C05F;
+}
+
+</style>

+ 1 - 1
client/components/Reader/CopyTextPage/CopyTextPage.vue

@@ -95,7 +95,7 @@ class CopyTextPage extends Vue {
     }
 
     keyHook(event) {
-        if (event.type == 'keydown' && (event.code == 'Escape')) {
+        if (event.type == 'keydown' && event.key == 'Escape') {
             this.close();
         }
         return true;

+ 8 - 4
client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue

@@ -22,15 +22,15 @@
         на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
         <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
 
-        <div v-show="mode == 'omnireader'">
+        <div v-show="mode == 'omnireader' || mode == 'liberama.top'">
             <p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
-                <br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
-                <q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
+                <br><strong>{{ bookmarkText }}</strong>
+                <q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
                     <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
                 </q-icon>
 
                 <br>или перетащив на панель закладок следующую ссылку:
-                <br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
+                <br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
                 <br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
                 <br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
             </p>
@@ -56,6 +56,10 @@ class CommonHelpPage extends Vue {
         return this.$store.state.config.mode;
     }
 
+    get bookmarkText() {
+        return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
+    }
+
     async copyText(text, mes) {
         const result = await copyTextToClipboard(text);
         const msg = (result ? mes : 'Копирование не удалось');

+ 1 - 1
client/components/Reader/HelpPage/HelpPage.vue

@@ -81,7 +81,7 @@ class HelpPage extends Vue {
     }
 
     keyHook(event) {
-        if (event.type == 'keydown' && (event.code == 'Escape')) {
+        if (event.type == 'keydown' && event.key == 'Escape') {
             this.close();
         }
         return true;

+ 131 - 0
client/components/Reader/LibsPage/LibsPage.vue

@@ -0,0 +1,131 @@
+<template>
+    <div class="hidden"></div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+import Window from '../../share/Window.vue';
+import * as utils from '../../../share/utils';
+//import rstore from '../../../store/modules/reader';
+
+export default @Component({
+    components: {
+        Window
+    },
+    watch: {
+        libs: function() {
+            this.sendLibs();
+        },
+    }    
+})
+class LibsPage extends Vue {
+    created() {
+        this.popupWindow = null;
+        this.commit = this.$store.commit;
+        this.messageListener = null;
+        //this.commit('reader/setLibs', rstore.libsDefaults);
+    }
+
+    init() {
+        if (this.mode != 'liberama.top')
+            return;
+
+        this.childReady = false;
+        const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
+        this.origin = `http://${subdomain}${window.location.host}`;
+
+        this.messageListener = (event) => {
+            if (event.origin !== this.origin)
+                return;
+
+            //console.log(event.data);
+
+            this.recvMessage(event.data);
+        };
+
+        this.popupWindow = window.open(`${this.origin}/#/external-libs`);
+
+        if (this.popupWindow) {
+
+            window.addEventListener('message', this.messageListener);
+
+            //Проверка закрытия окна
+            (async() => {
+                while(this.popupWindow) {
+                    if (this.popupWindow && this.popupWindow.closed)
+                        this.close();
+                    await utils.sleep(1000);
+                }
+            })();
+
+            //Установление связи с окном
+            (async() => {
+                let i = 0;
+                while(!this.childReady && this.popupWindow && i < 100) {
+                    this.sendMessage({type: 'mes', data: 'hello'});
+                    await utils.sleep(100);
+                    i++;
+                }
+                this.sendLibs();
+            })();
+        }
+    }
+
+    recvMessage(d) {
+        if (d.type == 'mes') {
+            switch(d.data) {
+                case 'ready':
+                    this.childReady = true;                    
+                    break;
+            }
+        } else if (d.type == 'libs') {
+            this.commit('reader/setLibs', d.data);
+        } else if (d.type == 'close') {
+            this.close();
+        } else if (d.type == 'submitUrl') {
+            this.$emit('load-book', d.data);
+            this.sendMessage({type: 'notify', data: 'Ссылка передана в читалку'});
+        }
+    }
+
+    sendMessage(d) {
+        if (this.popupWindow)
+            this.popupWindow.postMessage(Object.assign({}, {from: 'LibsPage'}, d), this.origin);
+    }
+
+    done() {
+        window.removeEventListener('message', this.messageListener);
+        if (this.popupWindow) {
+            this.popupWindow.close();
+            this.popupWindow = null;
+        }
+    }
+
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+
+    get libs() {
+        return this.$store.state.reader.libs;
+    }
+
+    sendLibs() {
+        this.sendMessage({type: 'libs', data: this.libs});
+    }
+
+    close() {
+        this.$emit('libs-close');
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.separator {
+    height: 1px;
+    background-color: #A0A0A0;
+}
+</style>

+ 7 - 5
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -31,7 +31,7 @@
             </q-btn>
 
             <div class="q-my-md"></div>
-            <div v-if="mode == 'omnireader'">
+            <!--div v-if="mode == 'omnireader'">
                 <div ref="yaShare2" class="ya-share2" 
                     data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
                     data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
@@ -39,7 +39,7 @@
                     data-url="https://omnireader.ru">
                 </div>
             </div>
-            <div class="q-my-sm"></div>
+            <div class="q-my-sm"></div-->
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
         </div>
@@ -82,8 +82,8 @@ class LoaderPage extends Vue {
 
     mounted() {
         this.progress = this.$refs.progress;
-        if (this.mode == 'omnireader')
-            Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
+        /*if (this.mode == 'omnireader')
+            Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
     }
 
     activated() {
@@ -93,6 +93,8 @@ class LoaderPage extends Vue {
     get title() {
         if (this.mode == 'omnireader')
             return 'Omni Reader - браузерная онлайн-читалка.';
+        if (this.mode == 'liberama.top')
+            return 'Liberama Reader - браузерная онлайн-читалка.';
         return 'Универсальная читалка книг и ресурсов интернета.';
 
     }
@@ -171,7 +173,7 @@ class LoaderPage extends Vue {
 
         //недостатки сторонних ui
         const input = this.$refs.input.$refs.input;
-        if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
+        if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
             this.submitUrl();
             return true;
         }

+ 1 - 1
client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue

@@ -78,7 +78,7 @@ class PasteTextPage extends Vue {
 
     keyHook(event) {
         if (event.type == 'keydown') {
-            switch (event.code) {
+            switch (event.key) {
                 case 'F2':
                     this.loadBuffer();
                     break;

+ 73 - 44
client/components/Reader/Reader.vue

@@ -2,10 +2,12 @@
     <div class="column no-wrap">
         <div ref="header" class="header" v-show="toolBarActive">
             <div ref="buttons" class="row justify-between no-wrap">
-                <button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
-                    <q-icon name="la la-arrow-left" size="32px"/>
-                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
-                </button>
+                <div>
+                    <button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
+                        <q-icon name="la la-arrow-left" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
+                    </button>
+                </div>
 
                 <div>
                     <button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
@@ -42,9 +44,9 @@
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
                     </button>
                     <div class="space"></div>
-                    <button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
-                        <q-icon name="la la-unlink" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
+                    <button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
+                        <q-icon name="la la-sitemap" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
                     </button>
                     <button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
                         <q-icon name="la la-book-open" size="32px"/>
@@ -52,10 +54,16 @@
                     </button>
                 </div>
 
-                <button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
-                    <q-icon name="la la-cog" size="32px"/>
-                    <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
-                </button>
+                <div>
+                    <button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
+                        <q-icon name="la la-unlink" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
+                    </button>
+                    <button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
+                        <q-icon name="la la-cog" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
+                    </button>
+                </div>
             </div>
         </div>
 
@@ -77,6 +85,7 @@
                 @stop-text-search="stopTextSearch">
             </SearchPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
+            <LibsPage v-show="libsActive" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
             <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
             <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
             <HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
@@ -159,6 +168,7 @@ import ProgressPage from './ProgressPage/ProgressPage.vue';
 import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
 import SearchPage from './SearchPage/SearchPage.vue';
 import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
+import LibsPage from './LibsPage/LibsPage.vue';
 import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
 import SettingsPage from './SettingsPage/SettingsPage.vue';
 import HelpPage from './HelpPage/HelpPage.vue';
@@ -181,6 +191,7 @@ export default @Component({
         SetPositionPage,
         SearchPage,
         CopyTextPage,
+        LibsPage,
         RecentBooksPage,
         SettingsPage,
         HelpPage,
@@ -230,6 +241,7 @@ export default @Component({
 class Reader extends Vue {
     rstore = {};
     loaderActive = false;
+    offlineModeActive = false;
     progressActive = false;
     fullScreenActive = false;
 
@@ -237,8 +249,8 @@ class Reader extends Vue {
     setPositionActive = false;
     searchActive = false;
     copyTextActive = false;
+    libsActive = false;
     recentBooksActive = false;
-    offlineModeActive = false;
     settingsActive = false;
     helpActive = false;
     clickMapActive = false;
@@ -410,7 +422,7 @@ class Reader extends Vue {
         await utils.sleep(3000);
         const today = utils.formatDate(new Date(), 'coDate');
 
-        if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
+        if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
             this.donationVisible = true;
         }
     }
@@ -587,7 +599,7 @@ class Reader extends Vue {
         }
     }
 
-    closeAllTextPages() {
+    closeAllWindows() {
         this.setPositionActive = false;
         this.copyTextActive = false;
         this.recentBooksActive = false;
@@ -600,7 +612,7 @@ class Reader extends Vue {
     loaderToggle() {
         this.loaderActive = !this.loaderActive;
         if (this.loaderActive) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
         }
     }
 
@@ -608,7 +620,7 @@ class Reader extends Vue {
         this.setPositionActive = !this.setPositionActive;
         const page = this.$refs.page;
         if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.setPositionActive = true;
 
             this.$nextTick(() => {
@@ -660,7 +672,7 @@ class Reader extends Vue {
         this.searchActive = !this.searchActive;
         const page = this.$refs.page;
         if (this.searchActive && this.activePage == 'TextPage' && page.parsed) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.searchActive = true;
 
             this.$nextTick(() => {
@@ -676,7 +688,7 @@ class Reader extends Vue {
         this.copyTextActive = !this.copyTextActive;
         const page = this.$refs.page;
         if (this.copyTextActive && this.activePage == 'TextPage' && page.parsed) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.copyTextActive = true;
 
             this.$nextTick(() => {
@@ -694,7 +706,7 @@ class Reader extends Vue {
     recentBooksToggle() {
         this.recentBooksActive = !this.recentBooksActive;
         if (this.recentBooksActive) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.$refs.recentBooksPage.init();
             this.recentBooksActive = true;
         } else {
@@ -702,6 +714,20 @@ class Reader extends Vue {
         }
     }
 
+    libsClose() {
+        if (this.libsActive)
+            this.libsToogle();
+    }
+
+    libsToogle() {
+        this.libsActive = !this.libsActive;
+        if (this.libsActive) {
+            this.$refs.libsPage.init();
+        } else {
+            this.$refs.libsPage.done();
+        }
+    }
+
     offlineModeToggle() {
         this.offlineModeActive = !this.offlineModeActive;
         this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
@@ -710,7 +736,7 @@ class Reader extends Vue {
     settingsToggle() {
         this.settingsActive = !this.settingsActive;
         if (this.settingsActive) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.settingsActive = true;
 
             this.$nextTick(() => {
@@ -724,7 +750,7 @@ class Reader extends Vue {
     helpToggle() {
         this.helpActive = !this.helpActive;
         if (this.helpActive) {
-            this.closeAllTextPages();
+            this.closeAllWindows();
             this.helpActive = true;
         }
     }
@@ -791,8 +817,9 @@ class Reader extends Vue {
             case 'search':
             case 'copyText':
             case 'refresh':
-            case 'offlineMode':
+            case 'libs':
             case 'recentBooks':
+            case 'offlineMode':
             case 'settings':
                 if (this.progressActive) {
                     classResult = classDisabled;
@@ -896,7 +923,7 @@ class Reader extends Vue {
             return;
         }
 
-        this.closeAllTextPages();
+        this.closeAllWindows();
 
         let url = encodeURI(decodeURI(opts.url));
 
@@ -1071,9 +1098,6 @@ class Reader extends Vue {
             case 'help':
                 this.helpToggle();
                 break;
-            case 'settings':
-                this.settingsToggle();
-                break;
             case 'undoAction':
                 this.undoAction();
                 break;
@@ -1101,12 +1125,18 @@ class Reader extends Vue {
             case 'refresh':
                 this.refreshBook();
                 break;
-            case 'offlineMode':
-                this.offlineModeToggle();
+            case 'libs':
+                this.libsToogle();
                 break;
             case 'recentBooks':
                 this.recentBooksToggle();
                 break;
+            case 'offlineMode':
+                this.offlineModeToggle();
+                break;
+            case 'settings':
+                this.settingsToggle();
+                break;
             case 'switchToolbar':
                 this.toolBarToggle();
                 break;
@@ -1173,29 +1203,28 @@ class Reader extends Vue {
             if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
                 return result;
 
-            let handled = false;
-            if (!handled && this.helpActive)
-                handled = this.$refs.helpPage.keyHook(event);
+            if (!result && this.helpActive)
+                result = this.$refs.helpPage.keyHook(event);
 
-            if (!handled && this.settingsActive)
-                handled = this.$refs.settingsPage.keyHook(event);
+            if (!result && this.settingsActive)
+                result = this.$refs.settingsPage.keyHook(event);
 
-            if (!handled && this.recentBooksActive)
-                handled = this.$refs.recentBooksPage.keyHook(event);
+            if (!result && this.recentBooksActive)
+                result = this.$refs.recentBooksPage.keyHook(event);
 
-            if (!handled && this.setPositionActive)
-                handled = this.$refs.setPositionPage.keyHook(event);
+            if (!result && this.setPositionActive)
+                result = this.$refs.setPositionPage.keyHook(event);
 
-            if (!handled && this.searchActive)
-                handled = this.$refs.searchPage.keyHook(event);
+            if (!result && this.searchActive)
+                result = this.$refs.searchPage.keyHook(event);
 
-            if (!handled && this.copyTextActive)
-                handled = this.$refs.copyTextPage.keyHook(event);
+            if (!result && this.copyTextActive)
+                result = this.$refs.copyTextPage.keyHook(event);
 
-            if (!handled && this.$refs.page && this.$refs.page.keyHook)
-                handled = this.$refs.page.keyHook(event);
+            if (!result && this.$refs.page && this.$refs.page.keyHook)
+                result = this.$refs.page.keyHook(event);
 
-            if (!handled && event.type == 'keydown') {
+            if (!result && event.type == 'keydown') {
                 const action = this.$root.readerActionByKeyEvent(event);
 
                 if (action == 'loader') {

+ 1 - 1
client/components/Reader/RecentBooksPage/RecentBooksPage.vue

@@ -341,7 +341,7 @@ class RecentBooksPage extends Vue {
     }
 
     keyHook(event) {
-        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
+        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
             this.close();
         }
         return true;

+ 1 - 1
client/components/Reader/SearchPage/SearchPage.vue

@@ -174,7 +174,7 @@ class SearchPage extends Vue {
     }
 
     keyHook(event) {
-        if (event.type == 'keydown' && (event.code == 'Escape')) {
+        if (event.type == 'keydown' && event.key == 'Escape') {
             this.close();
         }
         return true;

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

@@ -1,5 +1,5 @@
 <template>
-    <div></div>
+    <div class="hidden"></div>
 </template>
 
 <script>
@@ -35,6 +35,9 @@ export default @Component({
         currentProfile: function() {
             this.currentProfileChanged(true);
         },
+        libs: function() {
+            this.debouncedSaveLibs();
+        },
     },
 })
 class ServerStorage extends Vue {
@@ -49,12 +52,17 @@ class ServerStorage extends Vue {
             this.saveSettings();
         }, 500);
 
+        this.debouncedSaveLibs = _.debounce(() => {
+            this.saveLibs();
+        }, 500);
+
         this.debouncedNotifySuccess = _.debounce(() => {
             this.success('Данные синхронизированы с сервером');
         }, 1000);
 
         this.oldProfiles = {};
         this.oldSettings = {};
+        this.oldLibs = {};
     }
 
     async init() {
@@ -124,6 +132,8 @@ class ServerStorage extends Vue {
             await this.loadProfiles(force);
             this.checkCurrentProfile();
             await this.currentProfileChanged(force);
+            await this.loadLibs(force);
+
             const loadSuccess = await this.loadRecent();
             if (loadSuccess && force)
                 await this.saveRecent();
@@ -169,6 +179,14 @@ class ServerStorage extends Vue {
         return this.settings.showServerStorageMessages;
     }
 
+    get libs() {
+        return this.$store.state.reader.libs;
+    }
+
+    get libsRev() {
+        return this.$store.state.reader.libsRev;
+    }
+
     checkCurrentProfile() {
         if (!this.profiles[this.currentProfile]) {
             this.commit('reader/setCurrentProfile', '');
@@ -338,13 +356,85 @@ class ServerStorage extends Vue {
                 this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
             } else if (result.state == 'success') {
                 this.oldProfiles = _.cloneDeep(this.profiles);
-                this.commit('reader/setProfilesRev', this.profilesRev + 1);        
+                this.commit('reader/setProfilesRev', this.profilesRev + 1);
             }
         } finally {
             this.savingProfiles = false;
         }
     }
 
+    async loadLibs(force = false, doNotifySuccess = true) {
+        if (!this.keyInited || !this.serverSyncEnabled)
+            return;
+
+        const oldRev = this.libsRev;
+        //проверим ревизию на сервере
+        if (!force) {
+            try {
+                const revs = await this.storageCheck({libs: {}});
+                if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
+                    return;
+                }
+            } catch(e) {
+                this.error(`Ошибка соединения с сервером: ${e.message}`);
+                return;
+            }
+        }
+
+        let libs = null;
+        try {
+            libs = await this.storageGet({libs: {}});
+        } catch(e) {
+            this.error(`Ошибка соединения с сервером: ${e.message}`);
+            return;
+        }
+
+        if (libs.state == 'success') {
+            libs = libs.items.libs;
+
+            if (libs.rev == 0)
+                libs.data = {};
+
+            this.oldLibs = _.cloneDeep(libs.data);
+            this.commit('reader/setLibs', libs.data);
+            this.commit('reader/setLibsRev', libs.rev);
+
+            if (doNotifySuccess)
+                this.debouncedNotifySuccess();
+        } else {
+            this.warning(`Неверный ответ сервера: ${libs.state}`);
+        }
+    }
+
+    async saveLibs() {
+        if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
+            return;
+
+        const diff = utils.getObjDiff(this.oldLibs, this.libs);
+        if (utils.isEmptyObjDiff(diff))
+            return;
+
+        this.savingLibs = true;
+        try {
+            let result = {state: ''};
+            try {
+                result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
+            } catch(e) {
+                this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
+            }
+
+            if (result.state == 'reject') {
+                await this.loadLibs(true, false);
+                this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
+            } else if (result.state == 'success') {
+                this.oldLibs = _.cloneDeep(this.libs);
+                this.commit('reader/setLibsRev', this.libsRev + 1);
+            }
+        } finally {
+            this.savingLibs = false;
+        }
+    }
+
     async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
         if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
             return;

+ 1 - 1
client/components/Reader/SetPositionPage/SetPositionPage.vue

@@ -60,7 +60,7 @@ class SetPositionPage extends Vue {
     keyHook(event) {
         if (event.type == 'keydown') {
             const action = this.$root.readerActionByKeyEvent(event);
-            if (event.code == 'Escape' || action == 'setPosition') {
+            if (event.key == 'Escape' || action == 'setPosition') {
                 this.close();
             }
         }

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

@@ -499,7 +499,7 @@ class SettingsPage extends Vue {
     }
 
     keyHook(event) {
-        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
+        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
             this.close();
         }
         return true;

+ 4 - 2
client/components/Reader/SettingsPage/include/ButtonsTab.inc

@@ -1,8 +1,10 @@
 <div class="part-header">Показывать кнопки панели</div>
 
-<div class="item row" v-for="item in toolButtons" :key="item.name">
+<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
     <div class="label-3"></div>
     <div class="col row">
-        <q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]" />
+        <q-checkbox size="xs" @input="changeShowToolButton(item.name)"
+            :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
+        />
     </div>
 </div>

+ 1 - 1
client/components/Reader/SettingsPage/include/ProfilesTab.inc

@@ -68,7 +68,7 @@
                     <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
                 </q-icon>            
             </div>
-            <div v-if="mode == 'omnireader'">
+            <div v-if="mode == 'omnireader' || mode == 'liberama.top'">
                 <br>Переход по ссылке позволит автоматически ввести ключ доступа:
                 <br><div class="text-center" style="margin-top: 5px">
                     <a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>

+ 1 - 1
client/components/Reader/TextPage/TextPage.vue

@@ -839,7 +839,7 @@ class TextPage extends Vue {
             let i = this.pageLineCount;
             if (this.keepLastToFirst)
                 i--;
-            if (i >= 0 && this.linesDown.length >= 2*i) {
+            if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
                 this.currentAnimation = this.pageChangeAnimation;
                 this.pageChangeDirectionDown = true;
                 this.bookPos = this.linesDown[i].begin;

+ 10 - 5
client/components/Reader/share/BookParser.js

@@ -656,6 +656,7 @@ export default class BookParser {
         let style = {};
         let ofs = 0;//смещение от начала параграфа para.offset
         let imgW = 0;
+        let imageInPara = false;
         const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
         // тут начинается самый замес, перенос по слогам и стилизация, а также изображения
         for (const part of parts) {
@@ -664,7 +665,7 @@ export default class BookParser {
 
             //изображения
             if (part.image.id && !part.image.inline) {
-                parsed.visible = this.showImages;
+                imageInPara = true;
                 let bin = this.binary[part.image.id];
                 if (!bin)
                     bin = {h: 1, w: 1};
@@ -832,10 +833,14 @@ export default class BookParser {
         }
 
         //parsed.visible
-        parsed.visible = !(
-            (para.addIndex > this.addEmptyParagraphs) ||
-            (para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
-        );
+        if (imageInPara) {
+            parsed.visible = this.showImages;
+        } else {
+            parsed.visible = !(
+                (para.addIndex > this.addEmptyParagraphs) ||
+                (para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
+            );
+        }
 
         parsed.lines = lines;
         para.parsed = parsed;

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

@@ -1,4 +1,17 @@
 export const versionHistory = [
+{
+    showUntil: '2020-10-28',
+    header: '0.9.4 (2020-10-29)',
+    content:
+`
+<ul>
+    <li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
+    <li>для liberama.top добавлено новое окно: "Библиотека"</li>
+    <li>исправления багов</li>
+</ul>
+`
+},
+
 {
     showUntil: '2020-05-20',
     header: '0.9.3 (2020-05-21)',

+ 10 - 9
client/components/share/Notify.vue

@@ -19,11 +19,12 @@ class Notify extends Vue {
             iconColor = 'white',
             message = '',
             messageColor = 'black',
+            position = 'top-right',
         } = opts;
 
         caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
         return this.$q.notify({
-            position: 'top-right',
+            position,
             color,
             textColor: iconColor,
             icon,
@@ -38,20 +39,20 @@ class Notify extends Vue {
         });
     }
 
-    success(message, caption) {
-        this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
+    success(message, caption, options) {
+        this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
     }
 
-    warning(message, caption) {
-        this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
+    warning(message, caption, options) {
+        this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
     }
 
-    error(message, caption) {
-        this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
+    error(message, caption, options) {
+        this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
     }
 
-    info(message, caption) {
-        this.notify({color: 'info', icon: 'la la-bell', message, caption});
+    info(message, caption, options) {
+        this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
     }
 }
 //-----------------------------------------------------------------------------

+ 2 - 2
client/components/share/StdDialog.vue

@@ -284,12 +284,12 @@ class StdDialog extends Vue {
                     handled = true;
                 }
             } else {
-                if (event.code == 'Enter') {
+                if (event.key == 'Enter') {
                     this.okClick();
                     handled = true;
                 }
 
-                if (event.code == 'Escape') {
+                if (event.key == 'Escape') {
                     this.$nextTick(() => {
                         this.$refs.dialog.hide();
                     });

+ 1 - 0
client/components/share/Window.vue

@@ -5,6 +5,7 @@
                 <div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
                     @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
                     <span class="header-text col"><slot name="header"></slot></span>
+                    <slot name="buttons"></slot>
                     <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
                 </div>
 

+ 0 - 1
client/index.html.template

@@ -10,6 +10,5 @@
   </head>
   <body>
     <div id="app"></div>
-    <script src="https://yastatic.net/share2/share.js" async="async"></script>
   </body>
 </html>

+ 20 - 21
client/router.js

@@ -2,42 +2,41 @@ import Vue from 'vue';
 import VueRouter from 'vue-router';
 import _ from 'lodash';
 
-//немедленная загрузка
-import CardIndex from './components/CardIndex/CardIndex.vue';
-//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
-
+const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
 const Search = () => import('./components/CardIndex/Search/Search.vue');
 const Card = () => import('./components/CardIndex/Card/Card.vue');
 const Book = () => import('./components/CardIndex/Book/Book.vue');
 const History = () => import('./components/CardIndex/History/History.vue');
 
 //немедленная загрузка
-//const Reader = () => import('./components/Reader/Reader.vue');
-import Reader from './components/Reader/Reader.vue';
+//import Reader from './components/Reader/Reader.vue';
+const Reader = () => import('./components/Reader/Reader.vue');
+const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
 
-//const Forum = () => import('./components/Forum/Forum.vue');
 const Income = () => import('./components/Income/Income.vue');
 const Sources = () => import('./components/Sources/Sources.vue');
 const Settings = () => import('./components/Settings/Settings.vue');
 const Help = () => import('./components/Help/Help.vue');
-//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
+const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
 
 const myRoutes = [
     ['/', null, null, '/cardindex'],
-    ['/cardindex', CardIndex ],
-    ['/cardindex~search', Search ],
-    ['/cardindex~card', Card ],
-    ['/cardindex~card/:authorId', Card ],
-    ['/cardindex~book', Book ],
-    ['/cardindex~book/:bookId', Book ],
-    ['/cardindex~history', History ],
+    ['/cardindex', CardIndex],
+    ['/cardindex~search', Search],
+    ['/cardindex~card', Card],
+    ['/cardindex~card/:authorId', Card],
+    ['/cardindex~book', Book],
+    ['/cardindex~book/:bookId', Book],
+    ['/cardindex~history', History],
 
-    ['/reader', Reader ],
-    ['/income', Income ],
-    ['/sources', Sources ],
-    ['/settings', Settings ],
-    ['/help', Help ],
-    ['*', null, null, '/cardindex' ],
+    ['/reader', Reader],
+    ['/external-libs', ExternalLibs],
+    ['/income', Income],
+    ['/sources', Sources],
+    ['/settings', Settings],
+    ['/help', Help],
+    ['/404', NotFound404],
+    ['*', null, null, '/cardindex'],
 ];
 
 let routes = {};

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

@@ -12,6 +12,7 @@ const readerActions = {
     'copyText': 'Скопировать текст со страницы',
     'refresh': 'Принудительно обновить книгу',
     'offlineMode': 'Автономный режим (без интернета)',
+    'libs': 'Библиотека',
     'recentBooks': 'Открыть недавние',
     'switchToolbar': 'Показать/скрыть панель управления',
     'donate': '',
@@ -37,8 +38,9 @@ const toolButtons = [
     {name: 'search',      show: true},
     {name: 'copyText',    show: false},
     {name: 'refresh',     show: true},
-    {name: 'offlineMode', show: false},
+    {name: 'libs',        show: true},
     {name: 'recentBooks', show: true},
+    {name: 'offlineMode', show: false},
 ];
 
 //readerActions[name]
@@ -55,6 +57,7 @@ const hotKeys = [
     {name: 'copyText', codes: ['Ctrl+C']},
     {name: 'refresh', codes: ['R']},
     {name: 'offlineMode', codes: ['O']},
+    {name: 'libs', codes: ['L']},
     {name: 'recentBooks', codes: ['X']},
 
     {name: 'switchToolbar', codes: ['Tab', 'Q']},
@@ -250,6 +253,29 @@ const settingDefaults = {
     userHotKeys: {},
 };
 
+const libsDefaults = {
+    startLink: 'http://flibusta.is',
+    comment: 'Флибуста | Книжное братство',
+    closeAfterSubmit: false,
+    groups: [
+        {r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
+            {l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
+        ]},
+        {r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
+            {l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
+        ]},
+        {r: 'http://lib.ru', s: 'http://lib.ru', list: [
+            {l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
+        ]},
+        {r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
+            {l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
+        ]},
+        {r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
+            {l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
+        ]},
+    ]
+};
+
 for (const font of fonts)
     settingDefaults.fontShifts[font.name] = font.fontVertShift;
 for (const font of webFonts)
@@ -272,6 +298,8 @@ const state = {
     currentProfile: '',
     settings: Object.assign({}, settingDefaults),
     settingsRev: {},
+    libs: Object.assign({}, libsDefaults),
+    libsRev: 0,
 };
 
 // getters
@@ -315,6 +343,12 @@ const mutations = {
     setSettingsRev(state, value) {
         state.settingsRev = Object.assign({}, state.settingsRev, value);
     },
+    setLibs(state, value) {
+        state.libs = value;
+    },
+    setLibsRev(state, value) {
+        state.libsRev = value;
+    },
 };
 
 export default {
@@ -324,6 +358,7 @@ export default {
     fonts,
     webFonts,
     settingDefaults,
+    libsDefaults,
 
     namespaced: true,
     state,

+ 0 - 0
docs/beta.omnireader/beta.omnireader → docs/beta.omnireader.ru/beta.omnireader


+ 0 - 0
docs/beta.omnireader/deploy.sh → docs/beta.omnireader.ru/deploy.sh


+ 3 - 0
docs/beta.omnireader.ru/run_server.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+sudo -H -u www-data /home/beta.liberama/liberama

+ 0 - 11
docs/beta.omnireader/run_server.sh

@@ -1,11 +0,0 @@
-#!/bin/bash
-
-sudo -H -u www-data bash -c "\
-while true; do\
-  trap '' 2;\
-  cd /var/www;\
-  /home/beta.liberama/liberama;\
-  trap 2;\
-  echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
-  sleep 5;\
-done;"

+ 128 - 0
docs/liberama.top/liberama

@@ -0,0 +1,128 @@
+server {
+    server_name _;
+    listen 80 default_server;
+    listen 443 ssl default_server;
+
+    #openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt
+    ssl_certificate /etc/nginx/ssl/nginx.crt;
+    ssl_certificate_key /etc/nginx/ssl/nginx.key;
+    return 403;
+}
+
+server {
+  listen 443 ssl; # managed by Certbot
+  ssl_certificate /etc/letsencrypt/live/liberama.top/fullchain.pem; # managed by Certbot
+  ssl_certificate_key /etc/letsencrypt/live/liberama.top/privkey.pem; # managed by Certbot
+  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
+  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
+
+  server_name liberama.top;
+
+  client_max_body_size 50m;
+  proxy_read_timeout 1h;
+
+  gzip on;
+  gzip_min_length 1024;
+  gzip_proxied expired no-cache no-store private auth;
+  gzip_types *;
+
+  location /api {
+    proxy_pass http://127.0.0.1:55081;
+  }
+
+  location /ws {
+    proxy_pass http://127.0.0.1:55081;
+    proxy_http_version 1.1;
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+  }
+
+  location / {
+    root /home/liberama/public;
+
+    location /tmp {
+      types { } default_type "application/xml; charset=utf-8";
+      add_header Content-Encoding gzip;
+    }
+
+    location ~* \.(?:manifest|appcache|html)$ {
+      expires -1;
+    }
+  }
+}
+
+server {
+  listen 80;
+  server_name liberama.top;
+
+  return 301 https://$host$request_uri;
+}
+
+server {
+  listen 80;
+  server_name b.liberama.top;
+
+  client_max_body_size 50m;
+  proxy_read_timeout 1h;
+
+  gzip on;
+  gzip_min_length 1024;
+  gzip_proxied expired no-cache no-store private auth;
+  gzip_types *;
+
+  location /api {
+    proxy_pass http://127.0.0.1:55081;
+  }
+
+  location /ws {
+    proxy_pass http://127.0.0.1:55081;
+    proxy_http_version 1.1;
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+  }
+
+  location / {
+    root /home/liberama/public;
+
+    location /tmp {
+      types { } default_type "application/xml; charset=utf-8";
+      add_header Content-Encoding gzip;
+    }
+
+    location ~* \.(?:manifest|appcache|html)$ {
+      expires -1;
+    }
+  }
+}
+
+server {
+  listen 23480;
+  server_name flibusta_proxy;
+
+  valid_referers liberama.top b.liberama.top;
+
+  if ($invalid_referer) {
+    return 403;
+  }
+
+  location / {
+    proxy_pass http://flibusta.is;
+    proxy_redirect http://static.flibusta.is:443 http://b.liberama.top:23481;
+  }
+}
+
+server {
+  listen 23481;
+  server_name flibusta_proxy_static;
+
+  valid_referers liberama.top b.liberama.top;
+
+  if ($invalid_referer) {
+    return 403;
+  }
+
+  location / {
+    proxy_pass http://static.flibusta.is:443;
+    proxy_set_header Referer "";
+  }
+}

+ 12 - 6
docs/omnireader/README.md → docs/omnireader.ru/README.md

@@ -39,11 +39,11 @@ sudo apt install poppler-utils
 ```
 
 ### nginx, server config
-Для своего домена необходимо будет подправить docs/omnireader/omnireader.
+Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader.
 Можно также настроить сервер для HTTP, без SSL.
 ```
 sudo apt install nginx
-sudo cp docs/omnireader/omnireader /etc/nginx/sites-available/omnireader
+sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
 sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
 sudo rm /etc/nginx/sites-enabled/default
 sudo service nginx reload
@@ -59,14 +59,20 @@ sudo service php7.2-fpm restart
 
 sudo mkdir /home/oldreader
 sudo chown www-data.www-data /home/oldreader
-sudo -u www-data cp -r docs/omnireader/old/* /home/oldreader
+sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
+```
+
+## Запуск по крону
+```
+* * * * * /root/liberama/docs/omnireader/cron_server.sh
 ```
 
 ## Деплой и запуск
 ```
-cd docs/omnireader
+cd docs/omnireader.ru
+./stop_server.sh
 ./deploy.sh
-./run_server.sh
+./start_server.sh
 ```
 
 После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
@@ -81,4 +87,4 @@ cd docs/omnireader
         }
     ]
 ```
-и перезапустить `run_server.sh`
+и перезапустить сервер

+ 8 - 0
docs/omnireader.ru/cron_server.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+if ! pgrep -x "liberama" > /dev/null ; then
+    sudo -H -u www-data /home/liberama/liberama
+else
+    echo "Process 'liberama' already running"
+fi
+

+ 0 - 0
docs/omnireader/deploy.sh → docs/omnireader.ru/deploy.sh


+ 0 - 0
docs/omnireader/old/.htaccess → docs/omnireader.ru/old/.htaccess


+ 0 - 0
docs/omnireader/old/apple-touch-icon-precomposed.png → docs/omnireader.ru/old/apple-touch-icon-precomposed.png


+ 0 - 0
docs/omnireader/old/apple-touch-icon.png → docs/omnireader.ru/old/apple-touch-icon.png


+ 0 - 0
docs/omnireader/old/config/config.js → docs/omnireader.ru/old/config/config.js


+ 0 - 0
docs/omnireader/old/config/config.php → docs/omnireader.ru/old/config/config.php


+ 0 - 0
docs/omnireader/old/f.php → docs/omnireader.ru/old/f.php


+ 0 - 0
docs/omnireader/old/favicon.ico → docs/omnireader.ru/old/favicon.ico


+ 0 - 0
docs/omnireader/old/index.html → docs/omnireader.ru/old/index.html


+ 0 - 0
docs/omnireader/old/info.txt → docs/omnireader.ru/old/info.txt


+ 0 - 0
docs/omnireader/old/js/bpr319.js → docs/omnireader.ru/old/js/bpr319.js


+ 0 - 0
docs/omnireader/old/js/bpricon.gif → docs/omnireader.ru/old/js/bpricon.gif


+ 0 - 0
docs/omnireader/old/js/colo58.png → docs/omnireader.ru/old/js/colo58.png


+ 0 - 0
docs/omnireader/old/js/load.gif → docs/omnireader.ru/old/js/load.gif


+ 0 - 0
docs/omnireader/old/js/stylex.css → docs/omnireader.ru/old/js/stylex.css


+ 0 - 0
docs/omnireader/old/parser.php → docs/omnireader.ru/old/parser.php


+ 0 - 0
docs/omnireader/old/robots.txt → docs/omnireader.ru/old/robots.txt


+ 0 - 0
docs/omnireader/old/test.php → docs/omnireader.ru/old/test.php


+ 0 - 0
docs/omnireader/old/txt/.htaccess → docs/omnireader.ru/old/txt/.htaccess


+ 0 - 0
docs/omnireader/omnireader → docs/omnireader.ru/omnireader


+ 0 - 0
docs/omnireader/omnireader_http → docs/omnireader.ru/omnireader_http


+ 4 - 0
docs/omnireader.ru/start_server.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+sudo -H -u www-data /home/liberama/liberama &
+sudo service cron start

+ 4 - 0
docs/omnireader.ru/stop_server.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+sudo service cron stop
+sudo killall liberama

+ 0 - 11
docs/omnireader/run_server.sh

@@ -1,11 +0,0 @@
-#!/bin/bash
-
-sudo -H -u www-data bash -c "\
-while true; do\
-  trap '' 2;\
-  cd /var/www;\
-  /home/liberama/liberama;\
-  trap 2;\
-  echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
-  sleep 5;\
-done;"

+ 4 - 4
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.9.2",
+  "version": "0.9.4",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -12597,9 +12597,9 @@
       "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
     },
     "quasar": {
-      "version": "1.11.3",
-      "resolved": "https://registry.npmjs.org/quasar/-/quasar-1.11.3.tgz",
-      "integrity": "sha512-ZlWNn7nfo7dgh8pmhdYB5Jn9Sv/mFsYfoY0K/hYrCE/mJq2ZYWHTVCAFqbp1MEirbSz4RcRXu5kMa+sf/v/uAg=="
+      "version": "1.14.3",
+      "resolved": "https://registry.npmjs.org/quasar/-/quasar-1.14.3.tgz",
+      "integrity": "sha512-0baTygeaRhrOK9e+mtc32i6+xOnPKXaplgELJJdGWeoQBCwwfUX0tah0fgUmrdOEft3XmwhYW0a9yv/TAPUz7A=="
     },
     "querystring": {
       "version": "0.2.0",

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.9.3",
+  "version": "0.9.4",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",
@@ -72,7 +72,7 @@
     "multer": "^1.4.2",
     "pako": "^1.0.11",
     "path-browserify": "^1.0.0",
-    "quasar": "^1.11.3",
+    "quasar": "^1.14.3",
     "safe-buffer": "^5.2.0",
     "sjcl": "^1.0.8",
     "sql-template-strings": "^2.2.2",

+ 7 - 2
server/core/FileDecompressor.js

@@ -119,14 +119,19 @@ class FileDecompressor {
         try {
             return await zip.unpack(filename, outputDir, {
                 limitFileSize: this.limitFileSize, 
-                limitFileCount: 1000
-            });
+                limitFileCount: 1000,
+                decodeEntryNameCallback: (nameRaw) => {
+                    return utils.bufferRemoveZeroes(nameRaw);
+                }
+            }
+);
         } catch (e) {
             fs.emptyDir(outputDir);
             return await zip.unpack(filename, outputDir, {
                 limitFileSize: this.limitFileSize, 
                 limitFileCount: 1000,
                 decodeEntryNameCallback: (nameRaw) => {
+                    nameRaw = utils.bufferRemoveZeroes(nameRaw);
                     const enc = textUtils.getEncodingLite(nameRaw);
                     if (enc.indexOf('ISO-8859') < 0) {
                         return iconv.decode(nameRaw, enc);

+ 1 - 1
server/core/Zip/node_stream_zip.js

@@ -766,7 +766,7 @@ ZipEntry.prototype.readDataHeader = function(data) {
 };
 
 ZipEntry.prototype.read = function(data, offset) {
-    this.nameRaw = data.slice(offset, offset += this.fnameLen);
+    this.nameRaw = Buffer.from(data.slice(offset, offset += this.fnameLen));
     this.name = this.nameRaw.toString();
     var lastChar = data[offset - 1];
     this.isDirectory = (lastChar == 47) || (lastChar == 92);

+ 16 - 16
server/core/sax.js

@@ -93,6 +93,12 @@ function parseSync(xstr, options) {
             }
             tag = tag.toLowerCase();
 
+            if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
+                if (!cutCounter)
+                    cutTag = tag;
+                cutCounter++;
+            }
+
             let endTag = (singleTag ? tag : '');
             if (tag === '' || tag[0] !== '/') {
                 _onStartNode(tag, tail, singleTag, cutCounter, cutTag);
@@ -103,12 +109,6 @@ function parseSync(xstr, options) {
             if (endTag)
                 _onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
 
-            if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
-                if (!cutCounter)
-                    cutTag = tag;
-                cutCounter++;
-            }
-
             if (cutTag === endTag) {
                 cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
                 if (!cutCounter)
@@ -125,9 +125,9 @@ function parseSync(xstr, options) {
 
     if (i < len) {
         if (inCdata) {
-            _onCdata(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
+            _onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
         } else if (inComment) {
-            _onComment(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
+            _onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
         } else {
             _onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
         }
@@ -233,6 +233,12 @@ async function parse(xstr, options) {
             }
             tag = tag.toLowerCase();
 
+            if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
+                if (!cutCounter)
+                    cutTag = tag;
+                cutCounter++;
+            }
+
             let endTag = (singleTag ? tag : '');
             if (tag === '' || tag[0] !== '/') {
                 await _onStartNode(tag, tail, singleTag, cutCounter, cutTag);
@@ -243,12 +249,6 @@ async function parse(xstr, options) {
             if (endTag)
                 await _onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
 
-            if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
-                if (!cutCounter)
-                    cutTag = tag;
-                cutCounter++;
-            }
-
             if (cutTag === endTag) {
                 cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
                 if (!cutCounter)
@@ -265,9 +265,9 @@ async function parse(xstr, options) {
 
     if (i < len) {
         if (inCdata) {
-            await _onCdata(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
+            await _onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
         } else if (inComment) {
-            await _onComment(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
+            await _onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
         } else {
             await _onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
         }

+ 9 - 0
server/core/utils.js

@@ -14,6 +14,14 @@ function fromBase36(data) {
     return bs36.decode(data);
 }
 
+function bufferRemoveZeroes(buf) {
+    const i = buf.indexOf(0);
+    if (i >= 0) {
+        return buf.slice(0, i);
+    }
+    return buf;
+}
+
 function getFileHash(filename, hashName, enc) {
     return new Promise((resolve, reject) => {
         const hash = crypto.createHash(hashName);
@@ -86,6 +94,7 @@ function spawnProcess(cmd, opts) {
 module.exports = {
     toBase36,
     fromBase36,
+    bufferRemoveZeroes,
     getFileHash,
     sleep,
     randomHexString,