From 370e3db3455f548699ff5e046e0f8dcc304991ac Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Fri, 14 Feb 2020 09:19:53 +0100 Subject: viewer: major code and search mode overhaul Updated libraries to the lastest version SCSS Formatter as suggested VSC extensions Renamed toolbar-color by scrollbar-color LD components use Props in favor of touching the stores directly (when possible) Moved most common algorithms to a "services" folder Complete search overhaul (lots of code change) --- viewer/package-lock.json | 63 ++++++++--------- viewer/package.json | 14 ++-- viewer/src/assets/scss/global.scss | 9 ++- viewer/src/assets/scss/theme.scss | 15 ++-- viewer/src/components/LdBreadcrumb.vue | 17 +++-- viewer/src/components/LdCommand.vue | 16 ++--- viewer/src/components/LdCommandSearch.vue | 55 +++++++++++++++ viewer/src/components/LdGallery.vue | 47 +++++++++++++ viewer/src/components/LdModeRadio.vue | 41 ----------- viewer/src/components/LdPicture.vue | 2 +- viewer/src/components/LdProposition.vue | 35 +++++----- viewer/src/components/LdTagInput.vue | 75 +++----------------- viewer/src/components/LdThumbnail.vue | 4 +- viewer/src/dragscrollclickfix.ts | 52 -------------- viewer/src/locales/en.json | 10 +-- viewer/src/plugins/buefy.ts | 4 +- viewer/src/plugins/fontawesome.ts | 2 + viewer/src/plugins/router.ts | 17 ++--- viewer/src/services/dragscrollclickfix.ts | 52 ++++++++++++++ viewer/src/services/indexfactory.ts | 101 +++++++++++++++++++++++++++ viewer/src/services/indexsearch.ts | 70 +++++++++++++++++++ viewer/src/services/navigation.ts | 71 +++++++++++++++++++ viewer/src/store/galleryStore.ts | 51 +++----------- viewer/src/store/uiStore.ts | 22 +----- viewer/src/tools.ts | 59 ---------------- viewer/src/views/GalleryDirectory.vue | 14 ++-- viewer/src/views/GalleryNavigation.vue | 63 +++++++++++++++++ viewer/src/views/GallerySearch.vue | 36 +++++++--- viewer/src/views/MainGallery.vue | 110 ------------------------------ viewer/src/views/MainLayout.vue | 4 -- viewer/src/views/PanelLeft.vue | 34 ++++++++- viewer/src/views/PanelTop.vue | 7 +- viewer/visualstudio.code-workspace | 1 + 33 files changed, 655 insertions(+), 518 deletions(-) create mode 100644 viewer/src/components/LdCommandSearch.vue create mode 100644 viewer/src/components/LdGallery.vue delete mode 100644 viewer/src/components/LdModeRadio.vue delete mode 100644 viewer/src/dragscrollclickfix.ts create mode 100644 viewer/src/services/dragscrollclickfix.ts create mode 100644 viewer/src/services/indexfactory.ts create mode 100644 viewer/src/services/indexsearch.ts create mode 100644 viewer/src/services/navigation.ts delete mode 100644 viewer/src/tools.ts create mode 100644 viewer/src/views/GalleryNavigation.vue delete mode 100644 viewer/src/views/MainGallery.vue (limited to 'viewer') diff --git a/viewer/package-lock.json b/viewer/package-lock.json index 87825c8..955e21e 100644 --- a/viewer/package-lock.json +++ b/viewer/package-lock.json @@ -1283,12 +1283,12 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.19.2.tgz", - "integrity": "sha512-HX2qOq2GOV04HNrmKnTpSIpHjfl7iwdXe3u/Nvt+/cpmdvzYvY0NHSiTkYN257jHnq4OM/yo+OsFgati+7LqJA==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz", + "integrity": "sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.19.2", + "@typescript-eslint/experimental-utils": "2.20.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -1296,32 +1296,32 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.19.2.tgz", - "integrity": "sha512-B88QuwT1wMJR750YvTJBNjMZwmiPpbmKYLm1yI7PCc3x0NariqPwqaPsoJRwU9DmUi0cd9dkhz1IqEnwfD+P1A==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz", + "integrity": "sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.19.2", + "@typescript-eslint/typescript-estree": "2.20.0", "eslint-scope": "^5.0.0" } }, "@typescript-eslint/parser": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.19.2.tgz", - "integrity": "sha512-8uwnYGKqX9wWHGPGdLB9sk9+12sjcdqEEYKGgbS8A0IvYX59h01o8os5qXUHMq2na8vpDRaV0suTLM7S8wraTA==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.20.0.tgz", + "integrity": "sha512-o8qsKaosLh2qhMZiHNtaHKTHyCHc3Triq6aMnwnWj7budm3xAY9owSZzV1uon5T9cWmJRJGzTFa90aex4m77Lw==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.19.2", - "@typescript-eslint/typescript-estree": "2.19.2", + "@typescript-eslint/experimental-utils": "2.20.0", + "@typescript-eslint/typescript-estree": "2.20.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.19.2.tgz", - "integrity": "sha512-Xu/qa0MDk6upQWqE4Qy2X16Xg8Vi32tQS2PR0AvnT/ZYS4YGDvtn2MStOh5y8Zy2mg4NuL06KUHlvCh95j9C6Q==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz", + "integrity": "sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A==", "dev": true, "requires": { "debug": "^4.1.1", @@ -2883,9 +2883,9 @@ } }, "buefy": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.8.10.tgz", - "integrity": "sha512-Lw/UP3Ku7o+oqam9TIoRMG5SrytGQwXWAoxAtqt6Wb9eSsMEqp/5o+jZnz8oteR06YWgjdSIfOv2YeEdjEkQCg==", + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/buefy/-/buefy-0.8.12.tgz", + "integrity": "sha512-scKb+0piTAEYk8mopu5HzshlGsT0K9ChlfkGhQgAF7jC9lH3Ta7anYXG+l8uBoSpqgChK0H19jPP55FbeKn1nA==", "requires": { "bulma": "0.7.5" } @@ -4881,11 +4881,12 @@ } }, "eslint-plugin-vue": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.1.2.tgz", - "integrity": "sha512-M75oAB+2a/LNkLKRbeEaS07EjzjIUaV7/hYoHAfRFeeF8ZMmCbahUn8nQLsLP85mkar24+zDU3QW2iT1JRsACw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.1.tgz", + "integrity": "sha512-MiIDOotoWseIfLIfGeDzF6sDvHkVvGd2JgkvjyHtN3q4RoxdAXrAMuI3SXTOKatljgacKwpNAYShmcKZa4yZzw==", "dev": true, "requires": { + "natural-compare": "^1.4.0", "semver": "^5.6.0", "vue-eslint-parser": "^7.0.0" } @@ -11950,9 +11951,9 @@ "dev": true }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz", + "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==", "dev": true }, "uglify-js": { @@ -12248,9 +12249,9 @@ "dev": true }, "v-lazy-image": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/v-lazy-image/-/v-lazy-image-1.3.2.tgz", - "integrity": "sha512-yZYMLoy95S4K9mWE/2DMZcwvaWnGiAHGXcKRruyrFvAdFm2fsnfyL0yj2UwXEGliNZO7I4mRy9/RB7J4CT0HAQ==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/v-lazy-image/-/v-lazy-image-1.4.0.tgz", + "integrity": "sha512-Xp/fM786hdXlP10HatvtNsvvPjW6yOsH17lvVEEuGiNMnyiafT9XVomQRRM4t+IzN21cz3SQcw9cqd486yhpgQ==" }, "v8-compile-cache": { "version": "2.1.0", @@ -12303,9 +12304,9 @@ "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" }, "vue-class-component": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.2.tgz", - "integrity": "sha512-QjVfjRffux0rUBNtxr1hvUxDrfifDvk9q/OSdB/sKIlfxAudDF2E1YTeiEC+qOYIOOBGWkgSKQSnast6H+S38w==" + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.3.tgz", + "integrity": "sha512-oEqYpXKaFN+TaXU+mRLEx8dX0ah85aAJEe61mpdoUrq0Bhe/6sWhyZX1JjMQLhVsHAkncyhedhmCdDVSasUtDw==" }, "vue-cli-plugin-buefy": { "version": "0.3.7", diff --git a/viewer/package.json b/viewer/package.json index 212c2e6..7218cda 100644 --- a/viewer/package.json +++ b/viewer/package.json @@ -13,11 +13,11 @@ "@fortawesome/fontawesome-svg-core": "^1.2.27", "@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/vue-fontawesome": "^0.1.5", - "buefy": "^0.8.10", + "buefy": "^0.8.12", "core-js": "^3.6.4", - "v-lazy-image": "^1.3.2", + "v-lazy-image": "^1.4.0", "vue": "^2.6.10", - "vue-class-component": "^7.2.2", + "vue-class-component": "^7.2.3", "vue-dragscroll": "^2.0.1", "vue-i18n": "^8.0.0", "vue-property-decorator": "^8.4.0", @@ -27,8 +27,8 @@ }, "devDependencies": { "@types/webpack": "^4.41.6", - "@typescript-eslint/eslint-plugin": "^2.19.2", - "@typescript-eslint/parser": "^2.19.2", + "@typescript-eslint/eslint-plugin": "^2.20.0", + "@typescript-eslint/parser": "^2.20.0", "@vue/cli-plugin-babel": "^4.2.1", "@vue/cli-plugin-eslint": "^4.2.1", "@vue/cli-plugin-router": "^4.2.1", @@ -37,10 +37,10 @@ "@vue/cli-service": "^4.2.1", "@vue/eslint-config-typescript": "^5.0.1", "eslint": "^6.8.0", - "eslint-plugin-vue": "^6.1.2", + "eslint-plugin-vue": "^6.2.1", "node-sass": "^4.13.1", "sass-loader": "^8.0.2", - "typescript": "^3.7.5", + "typescript": "^3.8.2", "vue-cli-plugin-buefy": "^0.3.7", "vue-cli-plugin-fontawesome": "^0.2.0", "vue-cli-plugin-i18n": "^0.6.1", diff --git a/viewer/src/assets/scss/global.scss b/viewer/src/assets/scss/global.scss index ed69841..ea25513 100644 --- a/viewer/src/assets/scss/global.scss +++ b/viewer/src/assets/scss/global.scss @@ -27,6 +27,10 @@ color: red; } +button svg + span { + margin-left: 7px; +} + // === Tools .nowrap { @@ -66,11 +70,10 @@ // Disable sticky hover styling on touch devices, // on which the virtual cursor doesn't leave the element after being tapped. // The fix can be applied to `a` elements by using the .link class. -@media (hover:none), (hover:on-demand) { +@media (hover: none), (hover: on-demand) { .link:hover { color: $link !important; } - .disabled:hover { color: $disabled-color !important; } @@ -90,7 +93,7 @@ } .scrollbar::-webkit-scrollbar-thumb { box-shadow: inset 0 0 1px black; - background-color: $toolbar-color; + background-color: $scrollbar-color; } // === Thumbnail tiles alignment diff --git a/viewer/src/assets/scss/theme.scss b/viewer/src/assets/scss/theme.scss index 0a921a9..60504e3 100644 --- a/viewer/src/assets/scss/theme.scss +++ b/viewer/src/assets/scss/theme.scss @@ -18,9 +18,8 @@ -- along with this program. If not, see . */ -@import '_buefy_variables.scss'; -@import 'palette.scss'; - +@import "_buefy_variables.scss"; +@import "palette.scss"; // Buefy components @@ -44,7 +43,12 @@ $loading-background: $palette-800; $title-color: $palette-200; $title-size: $size-5; $tag-background-color: $palette-800; - +$button-color: $palette-100; +$button-background-color: $palette-700; +$button-border-color: $palette-500; +$button-focus-color: $button-color; +$button-focus-border-color: $link; +$button-focus-box-shadow-size: 0; // Custom components @@ -54,14 +58,13 @@ $panel-left-bgcolor: $palette-800; $panel-left-txtcolor: $primary; $command-buttons-bgcolor: $palette-700; $content-bgcolor: $palette-900; -$toolbar-color: $palette-300; // FIXME: should be named "scrollbar" +$scrollbar-color: $palette-300; $loader-color: $palette-800; $input-tag-delete-background-color: $palette-700; $breadcrumb-margins: 12px; $breadcrumb-overflow-mask-size: $breadcrumb-margins + 60px; $thumbnail-other-size: 120px; - // Layout $layout-top: 45px; diff --git a/viewer/src/components/LdBreadcrumb.vue b/viewer/src/components/LdBreadcrumb.vue index 7f7ef7d..643bfb6 100644 --- a/viewer/src/components/LdBreadcrumb.vue +++ b/viewer/src/components/LdBreadcrumb.vue @@ -29,24 +29,29 @@ >
diff --git a/viewer/src/components/LdCommand.vue b/viewer/src/components/LdCommand.vue index 7590ea7..468c241 100644 --- a/viewer/src/components/LdCommand.vue +++ b/viewer/src/components/LdCommand.vue @@ -23,14 +23,6 @@ - - - @@ -42,21 +34,23 @@ + + diff --git a/viewer/src/components/LdGallery.vue b/viewer/src/components/LdGallery.vue new file mode 100644 index 0000000..169bc54 --- /dev/null +++ b/viewer/src/components/LdGallery.vue @@ -0,0 +1,47 @@ + + + + + + + diff --git a/viewer/src/components/LdModeRadio.vue b/viewer/src/components/LdModeRadio.vue deleted file mode 100644 index c1d5702..0000000 --- a/viewer/src/components/LdModeRadio.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - diff --git a/viewer/src/components/LdPicture.vue b/viewer/src/components/LdPicture.vue index a5faeb3..1cfcc8b 100644 --- a/viewer/src/components/LdPicture.vue +++ b/viewer/src/components/LdPicture.vue @@ -39,7 +39,7 @@ diff --git a/viewer/src/components/LdTagInput.vue b/viewer/src/components/LdTagInput.vue index eff02e6..982abe4 100644 --- a/viewer/src/components/LdTagInput.vue +++ b/viewer/src/components/LdTagInput.vue @@ -19,7 +19,7 @@ diff --git a/viewer/src/dragscrollclickfix.ts b/viewer/src/dragscrollclickfix.ts deleted file mode 100644 index 38eb106..0000000 --- a/viewer/src/dragscrollclickfix.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* ldgallery - A static generator which turns a collection of tagged --- pictures into a searchable web gallery. --- --- Copyright (C) 2019-2020 Guillaume FOUET --- --- This program is free software: you can redistribute it and/or modify --- it under the terms of the GNU Affero General Public License as --- published by the Free Software Foundation, either version 3 of the --- License, or (at your option) any later version. --- --- This program is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU Affero General Public License for more details. --- --- You should have received a copy of the GNU Affero General Public License --- along with this program. If not, see . -*/ - -// https://github.com/donmbelembe/vue-dragscroll/issues/61 -export default class DragScrollClickFix { - - readonly DRAG_DELAY = 250; // This is the minimal delay to consider a click to be a drag, mostly usefull for touch devices - - timer: NodeJS.Timeout | null = null; - dragging: boolean = false; - - onDragScrollStart() { - this.timer = setTimeout(() => this.onTimer(), this.DRAG_DELAY); - } - - onTimer() { - this.timer = null; - this.dragging = true; - } - - onDragScrollEnd() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - setTimeout(() => this.dragging = false); - } - - onClickCapture(e: MouseEvent) { - if (this.dragging) { - this.dragging = false; - e.preventDefault(); - e.stopPropagation(); - } - } -} diff --git a/viewer/src/locales/en.json b/viewer/src/locales/en.json index 9440103..878204e 100644 --- a/viewer/src/locales/en.json +++ b/viewer/src/locales/en.json @@ -1,8 +1,6 @@ { "tagInput.placeholder": "Filters", "tagInput.nomatch": "No match", - "mode.navigation": "Navigation", - "mode.search": "Search", "search.no-results": "No results", "panelLeft.propositions": "Related filters", "tag-propositions.substraction": "Exclude items with this tag", @@ -11,7 +9,9 @@ "tag-propositions.item-count": "Item count", "gallery.unknowntype": "Unknown item type", "command.search": "Open/close search panel", - "command.home": "Go to gallery home", + "command.search.clear": "Clear", + "command.search.search": "Search", "command.back": "Go back", - "command.parent": "Go to parent directory" -} \ No newline at end of file + "command.parent": "Go to parent directory", + "directory.no-results": "Empty directory" +} diff --git a/viewer/src/plugins/buefy.ts b/viewer/src/plugins/buefy.ts index 74b6176..ebdf64e 100644 --- a/viewer/src/plugins/buefy.ts +++ b/viewer/src/plugins/buefy.ts @@ -24,7 +24,7 @@ import Taginput from 'buefy/src/components/taginput'; // @ts-ignore import Loading from 'buefy/src/components/loading'; // @ts-ignore -import Radio from 'buefy/src/components/radio'; +import Button from 'buefy/src/components/button'; // @ts-ignore import SnackBar from 'buefy/src/components/snackbar'; @@ -32,7 +32,7 @@ import "@/assets/scss/buefy.scss"; Vue.use(Taginput); Vue.use(Loading); -Vue.use(Radio); +Vue.use(Button); Vue.use(SnackBar); declare module 'vue/types/vue' { diff --git a/viewer/src/plugins/fontawesome.ts b/viewer/src/plugins/fontawesome.ts index e8848f9..cc8b7ab 100644 --- a/viewer/src/plugins/fontawesome.ts +++ b/viewer/src/plugins/fontawesome.ts @@ -23,6 +23,7 @@ import { library, config } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { faFolder, + faEraser, faSearch, faPlus, faMinus, @@ -37,6 +38,7 @@ import { library.add( faFolder, + faEraser, faSearch, faPlus, faMinus, diff --git a/viewer/src/plugins/router.ts b/viewer/src/plugins/router.ts index 8b4a8dc..00979c9 100644 --- a/viewer/src/plugins/router.ts +++ b/viewer/src/plugins/router.ts @@ -18,19 +18,20 @@ */ import Vue from "vue"; -import VueRouter from "vue-router"; -import MainGallery from "@/views/MainGallery.vue"; +import VueRouter, { RouteConfig } from "vue-router"; +import GalleryNavigation from "@/views/GalleryNavigation.vue"; Vue.use(VueRouter); -// async way : component: () => import(/* webpackChunkName: "MainGallery" */ "@/views/MainGallery.vue"), - -const routes = [ +const routes: RouteConfig[] = [ { path: "*", - name: "MainGallery", - component: MainGallery, - props: true + name: "GalleryNavigation", + component: GalleryNavigation, + props: (route) => ({ + path: route.params.pathMatch, + query: Object.keys(route.query), + }), }, ]; diff --git a/viewer/src/services/dragscrollclickfix.ts b/viewer/src/services/dragscrollclickfix.ts new file mode 100644 index 0000000..38eb106 --- /dev/null +++ b/viewer/src/services/dragscrollclickfix.ts @@ -0,0 +1,52 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2020 Guillaume FOUET +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +*/ + +// https://github.com/donmbelembe/vue-dragscroll/issues/61 +export default class DragScrollClickFix { + + readonly DRAG_DELAY = 250; // This is the minimal delay to consider a click to be a drag, mostly usefull for touch devices + + timer: NodeJS.Timeout | null = null; + dragging: boolean = false; + + onDragScrollStart() { + this.timer = setTimeout(() => this.onTimer(), this.DRAG_DELAY); + } + + onTimer() { + this.timer = null; + this.dragging = true; + } + + onDragScrollEnd() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + setTimeout(() => this.dragging = false); + } + + onClickCapture(e: MouseEvent) { + if (this.dragging) { + this.dragging = false; + e.preventDefault(); + e.stopPropagation(); + } + } +} diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts new file mode 100644 index 0000000..a6bc865 --- /dev/null +++ b/viewer/src/services/indexfactory.ts @@ -0,0 +1,101 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2020 Guillaume FOUET +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +*/ + +import { Operation } from '@/@types/Operation'; +import Navigation from '@/services/navigation'; + +export default class IndexFactory { + + public static generateTags(root: Gallery.Item | null): Tag.Index { + let tagsIndex: Tag.Index = {}; + if (root) IndexFactory.pushTagsForItem(tagsIndex, root); + return tagsIndex; + } + + // Pushes all tags for a root item (and its children) to the index + private static pushTagsForItem(tagsIndex: Tag.Index, item: Gallery.Item): void { + console.log("IndexingTagsFor: ", item.path); + if (item.properties.type === "directory") { + item.properties.items.forEach(item => this.pushTagsForItem(tagsIndex, item)); + return; // Directories are not indexed + } + for (const tag of item.tags) { + const parts = tag.split('.'); + let lastPart: string | null = null; + for (const part of parts) { + if (!tagsIndex[part]) tagsIndex[part] = { tag: part, tagfiltered: Navigation.normalize(part), items: [], children: {} }; + if (!tagsIndex[part].items.includes(item)) tagsIndex[part].items.push(item); + if (lastPart) tagsIndex[lastPart].children[part] = tagsIndex[part]; + lastPart = part; + } + } + } + + // --- + + + public static searchTags(tagsIndex: Tag.Index, filter: string): Tag.Search[] { + let search: Tag.Search[] = []; + if (tagsIndex && filter) { + const operation = IndexFactory.extractOperation(filter); + if (operation !== Operation.INTERSECTION) filter = filter.slice(1); + if (filter.includes(":")) { + const filterParts = filter.split(":"); + search = this.searchTagsFromFilterWithCategory(tagsIndex, operation, filterParts[0], filterParts[1]); + } else { + search = this.searchTagsFromFilter(tagsIndex, operation, filter); + } + } + return search; + } + + private static extractOperation(filter: string): Operation { + const first = filter.slice(0, 1); + switch (first) { + case Operation.ADDITION: + case Operation.SUBSTRACTION: + return first; + default: + return Operation.INTERSECTION; + } + } + + private static searchTagsFromFilterWithCategory( + tagsIndex: Tag.Index, + operation: Operation, + category: string, + disambiguation: string + ): Tag.Search[] { + disambiguation = Navigation.normalize(disambiguation); + return Object.values(tagsIndex) + .filter(node => node.tag.includes(category)) + .flatMap(node => + Object.values(node.children) + .filter(child => child.tagfiltered.includes(disambiguation)) + .map(child => ({ ...child, parent: node, operation, display: `${operation}${node.tag}:${child.tag}` })) + ); + } + + private static searchTagsFromFilter(tagsIndex: Tag.Index, operation: Operation, filter: string): Tag.Search[] { + filter = Navigation.normalize(filter); + return Object.values(tagsIndex) + .filter(node => node.tagfiltered.includes(filter)) + .map(node => ({ ...node, operation, display: `${operation}${node.tag}` })); + } +} diff --git a/viewer/src/services/indexsearch.ts b/viewer/src/services/indexsearch.ts new file mode 100644 index 0000000..3e73fb1 --- /dev/null +++ b/viewer/src/services/indexsearch.ts @@ -0,0 +1,70 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2020 Guillaume FOUET +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +*/ + +import { Operation } from '@/@types/Operation'; + +export default class IndexSearch { + + // Results of the search (by tags) + public static search(searchTags: Tag.Search[], rootPath: string): Gallery.Item[] { + const byOperation = this.extractTagsByOperation(searchTags); + const intersection = this.extractIntersection(byOperation); + const substraction = this.extractSubstraction(byOperation); + return this.aggregateAll(byOperation, intersection, substraction) + .filter(item => item.path.startsWith(rootPath)); + } + + private static extractTagsByOperation(searchTags: Tag.Search[]): Tag.SearchByOperation { + let byOperation: Tag.SearchByOperation = {}; + Object.values(Operation).forEach( + operation => (byOperation[operation] = searchTags.filter(tag => tag.operation === operation)) + ); + return byOperation; + } + + private static extractIntersection(byOperation: Tag.SearchByOperation): Set { + let intersection = new Set(); + if (byOperation[Operation.INTERSECTION].length > 0) { + byOperation[Operation.INTERSECTION] + .map(tag => tag.items) + .reduce((a, b) => a.filter(c => b.includes(c))) + .flatMap(items => items) + .forEach(item => intersection.add(item)); + } + return intersection; + } + + private static extractSubstraction(byOperation: Tag.SearchByOperation): Set { + let substraction = new Set(); + if (byOperation[Operation.SUBSTRACTION].length > 0) { + byOperation[Operation.SUBSTRACTION].flatMap(tag => tag.items).forEach(item => substraction.add(item)); + } + return substraction; + } + + private static aggregateAll( + byOperation: Tag.SearchByOperation, + intersection: Set, + substraction: Set + ): Gallery.Item[] { + byOperation[Operation.ADDITION].flatMap(tag => tag.items).forEach(item => intersection.add(item)); + substraction.forEach(item => intersection.delete(item)); + return [...intersection]; + } +} diff --git a/viewer/src/services/navigation.ts b/viewer/src/services/navigation.ts new file mode 100644 index 0000000..77fa47a --- /dev/null +++ b/viewer/src/services/navigation.ts @@ -0,0 +1,71 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2020 Guillaume FOUET +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as +-- published by the Free Software Foundation, either version 3 of the +-- License, or (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . +*/ + +export default class Navigation { + + // Searches for an item by path from a root item (navigation) + public static searchCurrentItemPath(root: Gallery.Item, path: string): Gallery.Item[] { + if (path === root.path) return [root]; + if (root.properties.type === "directory" && path.startsWith(root.path)) { + const itemChain = root.properties.items + .map(item => this.searchCurrentItemPath(item, path)) + .find(itemChain => itemChain.length > 0); + if (itemChain) return [root, ...itemChain]; + } + return []; + } + + + // Normalize a string to lowercase, no-accents + public static normalize(value: string) { + return value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); + } + + + public static checkType(item: Gallery.Item | null, type: Gallery.ItemType): boolean { + return item?.properties.type === type ?? false; + } + + public static directoriesFirst(items: Gallery.Item[]) { + return [ + ...items + .filter(child => Navigation.checkType(child, "directory")) + .sort((a, b) => a.title.localeCompare(b.title)), + + ...items + .filter(child => !Navigation.checkType(child, "directory")), + ]; + } + + public static getIcon(item: Gallery.Item): string { + if (item.path.length <= 1) return "home"; + switch (item.properties.type) { + case "picture": + return "image"; + case "directory": + return "folder"; + case "other": + default: + return "file"; + } + } +} diff --git a/viewer/src/store/galleryStore.ts b/viewer/src/store/galleryStore.ts index e7d70f8..d2b28dd 100644 --- a/viewer/src/store/galleryStore.ts +++ b/viewer/src/store/galleryStore.ts @@ -18,7 +18,8 @@ */ import { createModule, mutation, action } from "vuex-class-component"; -import Tools from '@/tools'; +import IndexFactory from '@/services/indexfactory'; +import Navigation from '@/services/navigation'; const VuexModule = createModule({ namespaced: "galleryStore", @@ -29,7 +30,7 @@ export default class GalleryStore extends VuexModule { config: Gallery.Config | null = null; galleryItemsRoot: Gallery.Item | null = null; - tags: Tag.Index = {}; + tagsIndex: Tag.Index = {}; currentPath: string = "/"; // --- @@ -42,8 +43,8 @@ export default class GalleryStore extends VuexModule { this.galleryItemsRoot = galleryItemsRoot; } - @mutation private setTags(tags: Tag.Index) { - this.tags = tags; + @mutation private setTagsIndex(tagsIndex: Tag.Index) { + this.tagsIndex = tagsIndex; } @mutation setCurrentPath(currentPath: string) { @@ -53,7 +54,7 @@ export default class GalleryStore extends VuexModule { get currentItemPath(): Gallery.Item[] { const root = this.galleryItemsRoot; if (root) - return GalleryStore.searchCurrentItemPath(root, this.currentPath); + return Navigation.searchCurrentItemPath(root, this.currentPath); return []; } @@ -67,7 +68,7 @@ export default class GalleryStore extends VuexModule { // Fetches the gallery's JSON config @action async fetchConfig() { return fetch(`${process.env.VUE_APP_DATA_URL}config.json`, { cache: "no-cache" }) - .then(config => config.json()) + .then(response => response.json()) .then(this.setConfig); } @@ -82,43 +83,7 @@ export default class GalleryStore extends VuexModule { // Indexes the gallery @action async indexTags() { - const root = this.galleryItemsRoot; - let index = {}; - if (root) GalleryStore.pushTagsForItem(index, root); - console.log("Index: ", index); - this.setTags(index); - } - - // --- - - // Pushes all tags for a root item (and its children) to the index - private static pushTagsForItem(index: Tag.Index, item: Gallery.Item) { - console.log("IndexingTagsFor: ", item.path); - if (item.properties.type === "directory") { - item.properties.items.forEach(item => this.pushTagsForItem(index, item)); - return; // Directories are not indexed - } - for (const tag of item.tags) { - const parts = tag.split('.'); - let lastPart: string | null = null; - for (const part of parts) { - if (!index[part]) index[part] = { tag: part, tagfiltered: Tools.normalize(part), items: [], children: {} }; - if (!index[part].items.includes(item)) index[part].items.push(item); - if (lastPart) index[lastPart].children[part] = index[part]; - lastPart = part; - } - } + this.setTagsIndex(IndexFactory.generateTags(this.galleryItemsRoot)); } - // Searches for an item by path from a root item (navigation) - private static searchCurrentItemPath(item: Gallery.Item, path: string): Gallery.Item[] { - if (path === item.path) return [item]; - if (item.properties.type === "directory" && path.startsWith(item.path)) { - const itemChain = item.properties.items - .map(item => this.searchCurrentItemPath(item, path)) - .find(itemChain => itemChain.length > 0); - if (itemChain) return [item, ...itemChain]; - } - return []; - } } diff --git a/viewer/src/store/uiStore.ts b/viewer/src/store/uiStore.ts index f7484de..5b6e1ca 100644 --- a/viewer/src/store/uiStore.ts +++ b/viewer/src/store/uiStore.ts @@ -28,18 +28,8 @@ export default class UIStore extends VuexModule { fullscreen: boolean = false; fullWidth: boolean = true; - mode: "navigation" | "search" = "navigation"; - currentTags: Tag.Search[] = []; - - // --- - - get isModeSearch() { - return this.mode === "search"; - } - - get isModeNavigation() { - return this.mode === "navigation"; - } + searchMode: boolean = false; + searchFilters: Tag.Search[] = []; // --- @@ -50,12 +40,4 @@ export default class UIStore extends VuexModule { @mutation toggleFullWidth() { this.fullWidth = !this.fullWidth; } - - @mutation setModeNavigation() { - this.mode = "navigation"; - } - - @mutation setModeSearch() { - this.mode = "search"; - } } diff --git a/viewer/src/tools.ts b/viewer/src/tools.ts deleted file mode 100644 index 80a7ef0..0000000 --- a/viewer/src/tools.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* ldgallery - A static generator which turns a collection of tagged --- pictures into a searchable web gallery. --- --- Copyright (C) 2019-2020 Guillaume FOUET --- --- This program is free software: you can redistribute it and/or modify --- it under the terms of the GNU Affero General Public License as --- published by the Free Software Foundation, either version 3 of the --- License, or (at your option) any later version. --- --- This program is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU Affero General Public License for more details. --- --- You should have received a copy of the GNU Affero General Public License --- along with this program. If not, see . -*/ - -export default class Tools { - - // Normalize a string to lowercase, no-accents - public static normalize(value: string) { - return value - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase(); - } - - - public static checkType(item: Gallery.Item | null, type: Gallery.ItemType): boolean { - return item?.properties.type === type ?? false; - } - - public static directoriesFirst(items: Gallery.Item[]) { - return [ - ...items - .filter(child => Tools.checkType(child, "directory")) - .sort((a, b) => a.title.localeCompare(b.title)), - - ...items - .filter(child => !Tools.checkType(child, "directory")), - ]; - } - - public static getIcon(item: Gallery.Item): string { - if (item.path.length <= 1) return "home"; - switch (item.properties.type) { - case "picture": - return "image"; - case "directory": - return "folder"; - case "other": - default: - return "file"; - } - } - -} diff --git a/viewer/src/views/GalleryDirectory.vue b/viewer/src/views/GalleryDirectory.vue index 162ef6e..6e68578 100644 --- a/viewer/src/views/GalleryDirectory.vue +++ b/viewer/src/views/GalleryDirectory.vue @@ -18,18 +18,12 @@ --> diff --git a/viewer/src/views/GalleryNavigation.vue b/viewer/src/views/GalleryNavigation.vue new file mode 100644 index 0000000..e09b292 --- /dev/null +++ b/viewer/src/views/GalleryNavigation.vue @@ -0,0 +1,63 @@ + + + + + + + diff --git a/viewer/src/views/GallerySearch.vue b/viewer/src/views/GallerySearch.vue index 97c5c66..eacbcdd 100644 --- a/viewer/src/views/GallerySearch.vue +++ b/viewer/src/views/GallerySearch.vue @@ -18,25 +18,41 @@ --> diff --git a/viewer/src/views/MainGallery.vue b/viewer/src/views/MainGallery.vue deleted file mode 100644 index 5767cce..0000000 --- a/viewer/src/views/MainGallery.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - diff --git a/viewer/src/views/MainLayout.vue b/viewer/src/views/MainLayout.vue index 63a1b83..272c045 100644 --- a/viewer/src/views/MainLayout.vue +++ b/viewer/src/views/MainLayout.vue @@ -24,10 +24,6 @@ - - - - diff --git a/viewer/src/views/PanelLeft.vue b/viewer/src/views/PanelLeft.vue index a61fe4a..fd117a6 100644 --- a/viewer/src/views/PanelLeft.vue +++ b/viewer/src/views/PanelLeft.vue @@ -19,17 +19,45 @@