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/src/services/dragscrollclickfix.ts | 52 +++++++++++++++ viewer/src/services/indexfactory.ts | 101 ++++++++++++++++++++++++++++++ viewer/src/services/indexsearch.ts | 70 +++++++++++++++++++++ viewer/src/services/navigation.ts | 71 +++++++++++++++++++++ 4 files changed, 294 insertions(+) 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 (limited to 'viewer/src/services') 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"; + } + } +} -- cgit v1.2.3