Browse Source

Работа над проектом

Book Pauk 2 years ago
parent
commit
77938aac04

+ 10 - 0
client/components/Api/Api.vue

@@ -156,6 +156,16 @@ class Api {
         return response;
     }
 
+    async getGenreTree() {
+        const response = await this.request({action: 'get-genre-tree'});
+
+        if (response.error) {
+            throw new Error(response.error);
+        }
+
+        return response;
+    }    
+
     async getConfig() {
         const response = await this.request({action: 'get-config'});
 

+ 38 - 3
client/components/Search/Search.vue

@@ -160,6 +160,8 @@
                 </q-btn>
             </template>
         </Dialog>
+
+        <SelectGenreDialog v-model="selectGenreDialogVisible" v-model:genre="genre" :genre-tree="genreTree" />
     </div>
 </template>
 
@@ -169,6 +171,7 @@ import vueComponent from '../vueComponent.js';
 import { reactive } from 'vue';
 
 import PageScroller from './PageScroller/PageScroller.vue';
+import SelectGenreDialog from './SelectGenreDialog/SelectGenreDialog.vue';
 import authorBooksStorage from './authorBooksStorage';
 import DivBtn from '../share/DivBtn.vue';
 import Dialog from '../share/Dialog.vue';
@@ -180,6 +183,7 @@ import _ from 'lodash';
 const componentOptions = {
     components: {
         PageScroller,
+        SelectGenreDialog,
         Dialog,
         DivBtn
     },
@@ -236,6 +240,8 @@ class Search {
     loadingMessage = '';
     loadingMessage2 = '';
     settingsDialogVisible = false;
+    selectGenreDialogVisible = false;
+
     page = 1;
     pageCount = 1;
 
@@ -262,6 +268,8 @@ class Search {
     totalFound = 0;
     bookRowsOnPage = 100;
     inpxHash = '';
+    genreTree = [];
+    genreTreeInpxHash = '';
 
     limitOptions = [
         {label: '10', value: 10},
@@ -338,7 +346,7 @@ class Search {
     }
 
     selectGenre() {
-        this.$root.stdDialog.alert('Выбор жанра');
+        this.selectGenreDialogVisible = true;
     }    
 
     selectLang() {
@@ -455,9 +463,8 @@ class Search {
         try {
             let result;
 
-            const key = `${authorId}-${this.inpxHash}`;
-
             if (this.abCacheEnabled) {
+                const key = `${authorId}-${this.inpxHash}`;
                 const data = await authorBooksStorage.getData(key);
                 if (data) {
                     result = JSON.parse(data);
@@ -516,6 +523,33 @@ class Search {
         }
     }
 
+    async updateGenreTreeIfNeeded() {
+        try {
+            if (this.genreTreeInpxHash !== this.inpxHash) {
+                let result;
+
+                if (this.abCacheEnabled) {
+                    const key = `genre-tree-${this.inpxHash}`;
+                    const data = await authorBooksStorage.getData(key);
+                    if (data) {
+                        result = JSON.parse(data);
+                    } else {
+                        result = await this.api.getGenreTree();
+
+                        await authorBooksStorage.setData(key, JSON.stringify(result));
+                    }
+                } else {
+                    result = await this.api.getGenreTree();
+                }
+
+                this.genreTree = result.genreTree;
+                this.genreTreeInpxHash = result.inpxHash;
+            }
+        } catch (e) {
+            this.$root.stdDialog.alert(e.message, 'Ошибка');
+        }
+    }
+
     async updateTableData() {
         let result = [];
 
@@ -591,6 +625,7 @@ class Search {
                     this.inpxHash = result.inpxHash;
 
                     this.searchResult = result;
+                    await this.updateGenreTreeIfNeeded();
                     await this.updateTableData();
                     this.scrollToTop();
                 } catch (e) {

+ 172 - 0
client/components/Search/SelectGenreDialog/SelectGenreDialog.vue

@@ -0,0 +1,172 @@
+<template>
+    <Dialog v-model="dialogVisible">
+        <template #header>
+            <div class="row items-center">
+                <div style="font-size: 130%">
+                    Выбрать жанры
+                </div>
+            </div>
+        </template>
+
+        <div class="col column" style="height: 500px; min-width: 400px">
+            <div class="row items-center top-panel bg-grey-3">
+                <q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти" clearable />
+            </div>
+
+            <div class="col fit tree">
+                <div v-show="nodes.length" class="checkbox-tick-all">
+                    <q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
+                </div>
+                <q-tree
+                    v-model:ticked="ticked"
+                    v-model:expanded="expanded"
+                    class="q-my-xs"
+                    :nodes="nodes"
+                    node-key="key"
+                    tick-strategy="leaf"
+                    selected-color="black"
+                    :filter="search"
+                    no-nodes-label="Жанров нет"
+                    no-results-label="Ничего не найдено"
+                >
+                </q-tree>
+            </div>
+        </div>
+
+        <template #footer>
+            <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                OK
+            </q-btn>
+        </template>
+    </Dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../vueComponent.js';
+
+import Dialog from '../../share/Dialog.vue';
+
+const componentOptions = {
+    components: {
+        Dialog
+    },
+    watch: {
+        modelValue(newValue) {
+            this.dialogVisible = newValue;
+            if (newValue)
+                this.init();//no await
+        },
+        dialogVisible(newValue) {
+            this.$emit('update:modelValue', newValue);
+        },
+        genre() {
+            this.updateTicked();
+        },
+        ticked() {
+            this.checkAllTicked();
+            this.updateGenre();
+        },
+    }
+};
+class GenreSelectDialog {
+    _options = componentOptions;
+    _props = {
+        modelValue: Boolean,
+        genre: {type: String, value: ''},
+        genreTree: Array,
+    };
+
+    dialogVisible = false;
+
+    search = '';
+    ticked = [];
+    expanded = [];
+    tickAll = false;
+    allKeys = [];
+
+    created() {
+    }
+
+    async init() {
+    }
+
+    get nodes() {
+        const result = [];
+
+        this.allKeys = [];
+        for (const section of this.genreTree) {
+            const rkey = `r-${section.name}`;
+            const sec = {label: section.name, key: rkey, children: []};
+
+            for (const g of section.value) {
+                sec.children.push({label: g.name, key: g.value});
+                this.allKeys.push(g.value);
+            }
+
+            result.push(sec);            
+        }
+
+        return result;
+    }
+
+    makeTickAll() {
+        if (this.tickAll) {
+            const newTicked = [];
+            for (const key of this.allKeys) {
+                newTicked.push(key);
+            }
+            this.ticked = newTicked;
+        } else {
+            this.ticked = [];
+        }
+    }
+
+    checkAllTicked() {
+        const ticked = new Set(this.ticked);
+
+        let newTickAll = !!(this.nodes.length);
+        for (const key of this.allKeys) {
+            if (!ticked.has(key)) {
+                newTickAll = false;
+                break;
+            }
+        }
+        this.tickAll = newTickAll;
+    }
+
+    updateTicked() {
+        this.ticked = this.genre.split(',').filter(s => s);
+    }
+
+    updateGenre() {
+        this.$emit('update:genre', this.ticked.join(','));
+    }
+
+    okClick() {
+        this.dialogVisible = false;
+    }
+}
+
+export default vueComponent(GenreSelectDialog);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.top-panel {
+    height: 50px;
+    border-bottom: 1px solid gray;
+    padding: 0 10px 0 12px;
+}
+
+.tree {
+    padding: 0px 10px 10px 10px;
+    overflow: auto;
+}
+
+.checkbox-tick-all {
+    border-bottom: 1px solid #bbbbbb;
+    margin-bottom: 7px;
+    padding: 5px 5px 2px 16px;
+}
+</style>

+ 2 - 2
client/quasar.js

@@ -31,7 +31,7 @@ import {QSelect} from 'quasar/src/components/select';
 //import {QPopupProxy} from 'quasar/src/components/popup-proxy';
 import {QDialog} from 'quasar/src/components/dialog';
 //import {QChip} from 'quasar/src/components/chip';
-//import {QTree} from 'quasar/src/components/tree';
+import {QTree} from 'quasar/src/components/tree';
 //import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
 
 //import {QExpansionItem} from 'quasar/src/components/expansion-item';
@@ -63,7 +63,7 @@ const components = {
     //QPopupProxy,
     QDialog,
     //QChip,
-    //QTree,
+    QTree,
     //QExpansionItem,
     //QVirtualScroll,
 };

+ 8 - 0
server/controllers/WebSocketController.js

@@ -70,6 +70,8 @@ class WebSocketController {
                     await this.search(req, ws); break;
                 case 'get-book-list':
                     await this.getBookList(req, ws); break;
+                case 'get-genre-tree':
+                    await this.getGenreTree(req, ws); break;
 
                 default:
                     throw new Error(`Action not found: ${req.action}`);
@@ -133,6 +135,12 @@ class WebSocketController {
 
         this.send(result, req, ws);
     }
+
+    async getGenreTree(req, ws) {
+        const result = await this.webWorker.getGenreTree();
+
+        this.send(result, req, ws);
+    }
 }
 
 module.exports = WebSocketController;

+ 4 - 1
server/core/DbCreator.js

@@ -275,7 +275,10 @@ class DbCreator {
             let genre = rec.genre || emptyFieldValue;
             genre = rec.genre.split(',');
 
-            for (const g of genre) {
+            for (let g of genre) {
+                if (!g)
+                    g = emptyFieldValue;
+
                 let genreRec;
                 if (genreMap.has(g)) {
                     const genreId = genreMap.get(g);

+ 39 - 0
server/core/WebWorker.js

@@ -1,5 +1,6 @@
 const os = require('os');
 const fs = require('fs-extra');
+const _ = require('lodash');
 
 const WorkerState = require('./WorkerState');
 const { JembaDbThread } = require('jembadb');
@@ -9,6 +10,7 @@ const DbSearcher = require('./DbSearcher');
 const ayncExit = new (require('./AsyncExit'))();
 const log = new (require('./AppLogger'))().log;//singleton
 const utils = require('./utils');
+const genreTree = require('./genres');
 
 //server states
 const ssNormal = 'normal';
@@ -203,6 +205,43 @@ class WebWorker {
         return await this.dbSearcher.getBookList(authorId);
     }
 
+    async getGenreTree() {
+        this.checkMyState();
+
+        const config = await this.dbConfig();
+
+        let result;
+        const db = this.db;
+        if (!db.wwCache.genres) {
+            const genres = _.cloneDeep(genreTree);
+            const last = genres[genres.length - 1];
+
+            const genreValues = new Set();
+            for (const section of genres) {
+                for (const g of section.value)
+                    genreValues.add(g.value);
+            }
+
+            //добавим к жанрам те, что нашлись при парсинге
+            const rows = await db.select({table: 'genre', map: `(r) => ({value: r.value})`});
+            for (const row of rows) {
+                if (!genreValues.has(row.value))
+                    last.value.push({name: row.value, value: row.value});
+            }
+
+            result = {
+                genreTree: genres,
+                inpxHash: (config.inpxHash ? config.inpxHash : ''),
+            };
+
+            db.wwCache.genres = result;
+        } else {
+            result = db.wwCache.genres;
+        }
+
+        return result;
+    }
+
     async logServerStats() {
         while (1) {// eslint-disable-line
             try {

+ 2 - 2
server/core/Genres/genres.json → server/core/genres/genresText.js

@@ -1,4 +1,4 @@
-"
+module.exports = `
 #---------- Список жанров fb2 ----------
 0.1 Фантастика
 0.2 Детективы и Триллеры
@@ -276,4 +276,4 @@
 0.22.251 tbg_secondary;Учебники и пособия для среднего и специального образования
 0.22.252 tbg_higher;Учебники и пособия ВУЗов
 #---------- 2021-07-22 11:35:50.469539----------
-"
+`;

+ 53 - 0
server/core/genres/index.js

@@ -0,0 +1,53 @@
+const genresText = require('./genresText.js');
+const genres = [];
+
+const sec2index = {};
+const lines = genresText.split('\n').map(l => l.trim());
+
+let index = 0;
+let other;//прочее в конец
+
+for (const line of lines) {
+    if (!line || line[0] == '#')
+        continue;
+
+    const p = line.indexOf(' ');
+    const num = line.substring(0, p).trim().split('.');
+    if (num.length < 2)
+        continue;
+
+    const section = `${num[0]}.${num[1]}`;
+    if (section == '0.0')
+        continue;
+
+    let name = line.substring(p + 1).trim();
+
+    if (num.length < 3) {//раздел
+        if (section == '0.20') {//прочее
+            other = {name, value: []};
+        } else {
+            if (sec2index[section] === undefined) {
+                if (!genres[index])
+                    genres[index] = {name, value: []};
+                sec2index[section] = index;
+                index++;
+            }
+        }
+    } else {//подраздел
+        const n = name.split(';').map(l => l.trim());
+
+        if (section == '0.20') {//прочее
+            other.value.push({name: n[1], value: n[0]});
+        } else {
+            const i = sec2index[section];
+            if (i !== undefined) {
+                genres[i].value.push({name: n[1], value: n[0]});
+            }
+        }
+    }
+}
+
+if (other)
+    genres.push(other);
+
+module.exports = genres;