From c9ac0c75d9273d3df86e66f2dad830146b7b1008 Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Thu, 1 Jul 2021 20:19:47 +0200 Subject: viewer: Upgraded dependencies for Vue 2 Removed obsolete deps core-js ; resize-observer-polyfill --- viewer/src/services/ldzoom.ts | 2 -- 1 file changed, 2 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index 22d4699..0fb0848 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -17,8 +17,6 @@ -- along with this program. If not, see . */ -// polyfill still required for IE and Safari, see https://caniuse.com/#feat=resizeobserver -import ResizeObserver from "resize-observer-polyfill"; import "hammerjs"; /** -- cgit v1.2.3 From 9165cc1efcf7791f78b61b2c51a9de651b1b09aa Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Fri, 2 Jul 2021 22:53:16 +0200 Subject: viewer: types normalization - gallery.d.ts GitHub: closes #301 --- viewer/src/services/indexfactory.ts | 13 +++++++------ viewer/src/services/indexsearch.ts | 17 +++++++++-------- viewer/src/services/itemComparators.ts | 13 +++++++------ viewer/src/services/ldzoom.ts | 7 ++++--- viewer/src/services/navigation.ts | 15 ++++++++------- 5 files changed, 35 insertions(+), 30 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts index 4b28a60..0c5fdc5 100644 --- a/viewer/src/services/indexfactory.ts +++ b/viewer/src/services/indexfactory.ts @@ -17,19 +17,20 @@ -- along with this program. If not, see . */ -import { Operation } from "@/@types/Operation"; +import { Item, RawTag } from "@/@types/gallery"; import { ItemType } from "@/@types/ItemType"; +import { Operation } from "@/@types/Operation"; import Navigation from "@/services/navigation"; export default class IndexFactory { - public static generateTags(root: Gallery.Item | null): Tag.Index { + public static generateTags(root: Item | null): Tag.Index { const 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 { + private static pushTagsForItem(tagsIndex: Tag.Index, item: Item): void { if (item.properties.type === ItemType.DIRECTORY) { item.properties.items.forEach(item => this.pushTagsForItem(tagsIndex, item)); return; // Directories are not indexed @@ -49,7 +50,7 @@ export default class IndexFactory { } } - private static pushPartToIndex(index: Tag.Node, part: string, item: Gallery.Item, rootPart: boolean): Tag.Node { + private static pushPartToIndex(index: Tag.Node, part: string, item: Item, rootPart: boolean): Tag.Node { if (!index) index = { tag: part, @@ -131,7 +132,7 @@ export default class IndexFactory { // --- - public static generateCategories(tagsIndex: Tag.Index, categoryTags?: Gallery.RawTag[]): Tag.Category[] { + public static generateCategories(tagsIndex: Tag.Index, categoryTags?: RawTag[]): Tag.Category[] { if (!categoryTags?.length) return [{ tag: "", index: tagsIndex }]; const tagsCategories: Tag.Category[] = []; @@ -149,7 +150,7 @@ export default class IndexFactory { return tagsCategories; } - private static isDiscriminantTagOnly(tags: Gallery.RawTag[], node: Tag.Node): boolean { + private static isDiscriminantTagOnly(tags: RawTag[], node: Tag.Node): boolean { return !tags.includes(node.tag) || !node.childPart; } } diff --git a/viewer/src/services/indexsearch.ts b/viewer/src/services/indexsearch.ts index 00f8cfc..eda1b27 100644 --- a/viewer/src/services/indexsearch.ts +++ b/viewer/src/services/indexsearch.ts @@ -17,11 +17,12 @@ -- along with this program. If not, see . */ +import { Item } from "@/@types/gallery"; import { Operation } from "@/@types/Operation"; export default class IndexSearch { // Results of the search (by tags) - public static search(searchTags: Tag.Search[]): Gallery.Item[] { + public static search(searchTags: Tag.Search[]): Item[] { const byOperation = this.extractTagsByOperation(searchTags); const intersection = this.extractIntersection(byOperation); const substraction = this.extractSubstraction(byOperation); @@ -36,8 +37,8 @@ export default class IndexSearch { return byOperation; } - private static extractIntersection(byOperation: Tag.SearchByOperation): Set { - const intersection = new Set(); + private static extractIntersection(byOperation: Tag.SearchByOperation): Set { + const intersection = new Set(); if (byOperation[Operation.INTERSECTION].length > 0) { byOperation[Operation.INTERSECTION] .map(tag => tag.items) @@ -48,8 +49,8 @@ export default class IndexSearch { return intersection; } - private static extractSubstraction(byOperation: Tag.SearchByOperation): Set { - const substraction = new Set(); + private static extractSubstraction(byOperation: Tag.SearchByOperation): Set { + const substraction = new Set(); if (byOperation[Operation.SUBSTRACTION].length > 0) { byOperation[Operation.SUBSTRACTION].flatMap(tag => tag.items).forEach(item => substraction.add(item)); } @@ -58,9 +59,9 @@ export default class IndexSearch { private static aggregateAll( byOperation: Tag.SearchByOperation, - intersection: Set, - substraction: Set - ): Gallery.Item[] { + intersection: Set, + substraction: Set + ): 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/itemComparators.ts b/viewer/src/services/itemComparators.ts index bd9accb..aceff79 100644 --- a/viewer/src/services/itemComparators.ts +++ b/viewer/src/services/itemComparators.ts @@ -16,14 +16,15 @@ -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . */ -import { TranslateResult } from "vue-i18n"; +import { Item, ItemSortStr } from "@/@types/gallery"; import i18n from "@/plugins/i18n"; +import { TranslateResult } from "vue-i18n"; -export type ItemComparator = (left: Gallery.Item, right: Gallery.Item) => number; +export type ItemComparator = (left: Item, right: Item) => number; export type ItemSort = { text: TranslateResult; fn: ItemComparator }; export default class ItemComparators { - static readonly ITEM_SORTS: Record = { + static readonly ITEM_SORTS: Record = { title_asc: { text: i18n.t("command.sort.byTitleAsc"), fn: ItemComparators.chain(ItemComparators.sortByTitleAsc, ItemComparators.sortByPathAsc), @@ -40,7 +41,7 @@ export default class ItemComparators { static readonly DEFAULT = ItemComparators.ITEM_SORTS.date_asc; - static sortByPathAsc(left: Gallery.Item, right: Gallery.Item): number { + static sortByPathAsc(left: Item, right: Item): number { return left.path.localeCompare(right.path, undefined, { sensitivity: "base", ignorePunctuation: true, @@ -48,7 +49,7 @@ export default class ItemComparators { }); } - static sortByTitleAsc(left: Gallery.Item, right: Gallery.Item): number { + static sortByTitleAsc(left: Item, right: Item): number { return left.title.localeCompare(right.title, undefined, { sensitivity: "base", ignorePunctuation: true, @@ -56,7 +57,7 @@ export default class ItemComparators { }); } - static sortByDateAsc(left: Gallery.Item, right: Gallery.Item): number { + static sortByDateAsc(left: Item, right: Item): number { return left.datetime.localeCompare(right.datetime); // TODO: handle timezones } diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts index 0fb0848..33a64c8 100644 --- a/viewer/src/services/ldzoom.ts +++ b/viewer/src/services/ldzoom.ts @@ -17,6 +17,7 @@ -- along with this program. If not, see . */ +import { PictureProperties, Resolution } from "@/@types/gallery"; import "hammerjs"; /** @@ -25,7 +26,7 @@ import "hammerjs"; export default class LdZoom { readonly containerElement: HTMLDivElement; readonly imageElement: HTMLImageElement; - readonly pictureProperties: Gallery.PictureProperties; + readonly pictureProperties: PictureProperties; readonly maxScaleFactor: number; readonly scrollZoomSpeed: number; scaleFactor: number = 0.0; @@ -33,7 +34,7 @@ export default class LdZoom { constructor( containerElement: HTMLDivElement, imageElement: HTMLImageElement, - pictureProperties: Gallery.PictureProperties, + pictureProperties: PictureProperties, maxScaleFactor: number, scrollZoomSpeed: number ) { @@ -83,7 +84,7 @@ export default class LdZoom { /** * Returns the picture resolution as it should be displayed. */ - private getDisplayResolution(): Gallery.Resolution { + private getDisplayResolution(): Resolution { return { width: this.pictureProperties.resolution.width * this.scaleFactor, height: this.pictureProperties.resolution.height * this.scaleFactor, diff --git a/viewer/src/services/navigation.ts b/viewer/src/services/navigation.ts index 5b0716d..9bbd90c 100644 --- a/viewer/src/services/navigation.ts +++ b/viewer/src/services/navigation.ts @@ -17,6 +17,7 @@ -- along with this program. If not, see . */ +import { DirectoryItem, Item } from "@/@types/gallery"; import { ItemType } from "@/@types/ItemType"; export default class Navigation { @@ -31,7 +32,7 @@ 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[] { + public static searchCurrentItemPath(root: Item, path: string): Item[] { if (path === root.path) return [root]; if (root.properties.type === ItemType.DIRECTORY && path.startsWith(root.path)) { const itemChain = root.properties.items @@ -51,20 +52,20 @@ export default class Navigation { } // Checks if the type of an item matches - public static checkType(item: Gallery.Item | null, type: ItemType | null): boolean { + public static checkType(item: Item | null, type: ItemType | null): boolean { return (item?.properties.type ?? null) === type; } - public static getLastDirectory(itemPath: Gallery.Item[]): Gallery.Directory { + public static getLastDirectory(itemPath: Item[]): DirectoryItem { for (let idx = itemPath.length - 1; idx >= 0; idx--) { const item = itemPath[idx]; - if (Navigation.checkType(item, ItemType.DIRECTORY)) return item as Gallery.Directory; + if (Navigation.checkType(item, ItemType.DIRECTORY)) return item as DirectoryItem; } throw new Error("No directory found"); } // Sort a list of items, moving the directories to the beginning of the list - public static directoriesFirst(items: Gallery.Item[]) { + public static directoriesFirst(items: Item[]) { return [ ...items .filter(child => Navigation.checkType(child, ItemType.DIRECTORY)) @@ -75,13 +76,13 @@ export default class Navigation { } // Get the icon for an item - public static getIcon(item: Gallery.Item): string { + public static getIcon(item: Item): string { if (item.path.length <= 1) return "home"; return Navigation.ICON_BY_TYPE[item.properties.type]; } // Get the file name of an item, without its cache timestamp - public static getFileName(item: Gallery.Item): string { + public static getFileName(item: Item): string { if (item.properties.type === ItemType.DIRECTORY) return item.title; const timeStamped = item.properties.resource.split("/").pop() ?? ""; return timeStamped.split("?")[0]; -- cgit v1.2.3 From 92cb34b719b481faf417760f307241e8f6d777a9 Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Fri, 2 Jul 2021 22:59:56 +0200 Subject: viewer: types normalization - tag.d.ts GitHub: closes #301 --- viewer/src/services/indexfactory.ts | 29 +++++++++++++++-------------- viewer/src/services/indexsearch.ts | 13 +++++++------ 2 files changed, 22 insertions(+), 20 deletions(-) (limited to 'viewer/src/services') diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts index 0c5fdc5..691a765 100644 --- a/viewer/src/services/indexfactory.ts +++ b/viewer/src/services/indexfactory.ts @@ -20,17 +20,18 @@ import { Item, RawTag } from "@/@types/gallery"; import { ItemType } from "@/@types/ItemType"; import { Operation } from "@/@types/Operation"; +import { TagCategory, TagIndex, TagNode, TagSearch } from "@/@types/tag"; import Navigation from "@/services/navigation"; export default class IndexFactory { - public static generateTags(root: Item | null): Tag.Index { - const tagsIndex: Tag.Index = {}; + public static generateTags(root: Item | null): TagIndex { + const tagsIndex: TagIndex = {}; 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: Item): void { + private static pushTagsForItem(tagsIndex: TagIndex, item: Item): void { if (item.properties.type === ItemType.DIRECTORY) { item.properties.items.forEach(item => this.pushTagsForItem(tagsIndex, item)); return; // Directories are not indexed @@ -50,7 +51,7 @@ export default class IndexFactory { } } - private static pushPartToIndex(index: Tag.Node, part: string, item: Item, rootPart: boolean): Tag.Node { + private static pushPartToIndex(index: TagNode, part: string, item: Item, rootPart: boolean): TagNode { if (!index) index = { tag: part, @@ -69,8 +70,8 @@ export default class IndexFactory { // --- - public static searchTags(tagsIndex: Tag.Index, filter: string, strict: boolean): Tag.Search[] { - let search: Tag.Search[] = []; + public static searchTags(tagsIndex: TagIndex, filter: string, strict: boolean): TagSearch[] { + let search: TagSearch[] = []; if (tagsIndex && filter) { const operation = IndexFactory.extractOperation(filter); if (operation !== Operation.INTERSECTION) filter = filter.slice(1); @@ -96,12 +97,12 @@ export default class IndexFactory { } private static searchTagsFromFilterWithCategory( - tagsIndex: Tag.Index, + tagsIndex: TagIndex, operation: Operation, category: string, disambiguation: string, strict: boolean - ): Tag.Search[] { + ): TagSearch[] { category = Navigation.normalize(category); disambiguation = Navigation.normalize(disambiguation); return Object.values(tagsIndex) @@ -114,28 +115,28 @@ export default class IndexFactory { } private static searchTagsFromFilter( - tagsIndex: Tag.Index, + tagsIndex: TagIndex, operation: Operation, filter: string, strict: boolean - ): Tag.Search[] { + ): TagSearch[] { filter = Navigation.normalize(filter); return Object.values(tagsIndex) .filter(node => IndexFactory.matches(node, filter, strict)) .map(node => ({ ...node, operation, display: `${operation}${node.tag}` })); } - private static matches(node: Tag.Node, filter: string, strict: boolean): boolean { + private static matches(node: TagNode, filter: string, strict: boolean): boolean { if (strict) return node.tagfiltered === filter; return node.tagfiltered.includes(filter); } // --- - public static generateCategories(tagsIndex: Tag.Index, categoryTags?: RawTag[]): Tag.Category[] { + public static generateCategories(tagsIndex: TagIndex, categoryTags?: RawTag[]): TagCategory[] { if (!categoryTags?.length) return [{ tag: "", index: tagsIndex }]; - const tagsCategories: Tag.Category[] = []; + const tagsCategories: TagCategory[] = []; const tagsRemaining = new Map(Object.entries(tagsIndex)); categoryTags .map(tag => ({ tag, index: tagsIndex[tag]?.children })) @@ -150,7 +151,7 @@ export default class IndexFactory { return tagsCategories; } - private static isDiscriminantTagOnly(tags: RawTag[], node: Tag.Node): boolean { + private static isDiscriminantTagOnly(tags: RawTag[], node: TagNode): boolean { return !tags.includes(node.tag) || !node.childPart; } } diff --git a/viewer/src/services/indexsearch.ts b/viewer/src/services/indexsearch.ts index eda1b27..57bd03c 100644 --- a/viewer/src/services/indexsearch.ts +++ b/viewer/src/services/indexsearch.ts @@ -19,25 +19,26 @@ import { Item } from "@/@types/gallery"; import { Operation } from "@/@types/Operation"; +import { TagSearch, TagSearchByOperation } from "@/@types/tag"; export default class IndexSearch { // Results of the search (by tags) - public static search(searchTags: Tag.Search[]): Item[] { + public static search(searchTags: TagSearch[]): Item[] { const byOperation = this.extractTagsByOperation(searchTags); const intersection = this.extractIntersection(byOperation); const substraction = this.extractSubstraction(byOperation); return this.aggregateAll(byOperation, intersection, substraction); } - private static extractTagsByOperation(searchTags: Tag.Search[]): Tag.SearchByOperation { - const byOperation: Tag.SearchByOperation = {}; + private static extractTagsByOperation(searchTags: TagSearch[]): TagSearchByOperation { + const byOperation: TagSearchByOperation = {}; Object.values(Operation).forEach( operation => (byOperation[operation] = searchTags.filter(tag => tag.operation === operation)) ); return byOperation; } - private static extractIntersection(byOperation: Tag.SearchByOperation): Set { + private static extractIntersection(byOperation: TagSearchByOperation): Set { const intersection = new Set(); if (byOperation[Operation.INTERSECTION].length > 0) { byOperation[Operation.INTERSECTION] @@ -49,7 +50,7 @@ export default class IndexSearch { return intersection; } - private static extractSubstraction(byOperation: Tag.SearchByOperation): Set { + private static extractSubstraction(byOperation: TagSearchByOperation): Set { const substraction = new Set(); if (byOperation[Operation.SUBSTRACTION].length > 0) { byOperation[Operation.SUBSTRACTION].flatMap(tag => tag.items).forEach(item => substraction.add(item)); @@ -58,7 +59,7 @@ export default class IndexSearch { } private static aggregateAll( - byOperation: Tag.SearchByOperation, + byOperation: TagSearchByOperation, intersection: Set, substraction: Set ): Item[] { -- cgit v1.2.3 From 7714815396b4e86afc5db1af3726fab4edf0ca35 Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Sat, 3 Jul 2021 23:07:09 +0200 Subject: viewer: add markdown rendering component Extracted from b170f49 (GH PR #304) --- viewer/src/services/fetchWithCheck.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 viewer/src/services/fetchWithCheck.ts (limited to 'viewer/src/services') diff --git a/viewer/src/services/fetchWithCheck.ts b/viewer/src/services/fetchWithCheck.ts new file mode 100644 index 0000000..e84e8b6 --- /dev/null +++ b/viewer/src/services/fetchWithCheck.ts @@ -0,0 +1,7 @@ +export default class FetchWithCheck { + static async get(url: RequestInfo): Promise { + const response = await fetch(url); + if (!response.ok) throw new Error(`${response.status}: ${response.statusText}`); + return response; + } +} -- cgit v1.2.3 From 2bd63dc7622c0f84383fb76f7e2c4380d3f78495 Mon Sep 17 00:00:00 2001 From: Zero~Informatique Date: Sat, 3 Jul 2021 23:07:09 +0200 Subject: viewer: register markdown item type Extracted from b170f49 (GH PR #304) --- viewer/src/services/navigation.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'viewer/src/services') diff --git a/viewer/src/services/navigation.ts b/viewer/src/services/navigation.ts index 9bbd90c..5dcea88 100644 --- a/viewer/src/services/navigation.ts +++ b/viewer/src/services/navigation.ts @@ -25,6 +25,7 @@ export default class Navigation { directory: "folder", picture: "image", plaintext: "file-alt", + markdown: "file-alt", pdf: "file-pdf", video: "file-video", audio: "file-audio", -- cgit v1.2.3 From 00510820a2794efcadbc83f7f8b54318fe198ecb Mon Sep 17 00:00:00 2001 From: Zéro~Informatique Date: Tue, 26 Jul 2022 08:44:34 +0200 Subject: viewer: migrate to vue 3, general refactoring and cleanup Non-exhaustive list of fixes and improvements done at the same time: - html default background to grey (avoids white flash during init) - unified links behavior - added more theme variables - removed the flex-expand transition (it wasn't working) and replaced it with a slide - fixed LdLoading not centered on the content - title on removable tags - fixed an issue with encoded URI from vue-router - unified Item resource URLs - removed the iframe for PlainTextViewer (it wasn't working properly) and replaced it with a pre - fixed clear and search buttons tabindex - fixed the information panel bumping up during the fade animation of tag's dropdown - fixed some focus outlines not appearing correctly - moved CSS variables to the :root context - Code cleaning GitHub: closes #217 GitHub: closes #300 GitHub: closes #297 GitHub: closes #105 GitHub: closes #267 GitHub: closes #275 GitHub: closes #228 GitHub: closes #215 GitHub: closes #112 --- viewer/src/services/api/ldFetch.ts | 35 ++++++ viewer/src/services/dragscrollclickfix.ts | 51 --------- viewer/src/services/fetchWithCheck.ts | 7 -- viewer/src/services/indexFactory.ts | 163 ++++++++++++++++++++++++++++ viewer/src/services/indexSearch.ts | 74 +++++++++++++ viewer/src/services/indexfactory.ts | 157 --------------------------- viewer/src/services/indexsearch.ts | 70 ------------ viewer/src/services/itemComparator.ts | 93 ++++++++++++++++ viewer/src/services/itemComparators.ts | 74 ------------- viewer/src/services/itemGuards.ts | 11 ++ viewer/src/services/ldzoom.ts | 135 ----------------------- viewer/src/services/navigation.ts | 90 ++++++++------- viewer/src/services/ui/ldFullscreen.ts | 41 +++++++ viewer/src/services/ui/ldItemResourceUrl.ts | 15 +++ viewer/src/services/ui/ldKeepFocus.ts | 34 ++++++ viewer/src/services/ui/ldKeyboard.ts | 28 +++++ viewer/src/services/ui/ldSaveScroll.ts | 37 +++++++ viewer/src/services/ui/ldTitle.ts | 34 ++++++ viewer/src/services/ui/ldZoom.ts | 128 ++++++++++++++++++++++ 19 files changed, 736 insertions(+), 541 deletions(-) create mode 100644 viewer/src/services/api/ldFetch.ts delete mode 100644 viewer/src/services/dragscrollclickfix.ts delete mode 100644 viewer/src/services/fetchWithCheck.ts create mode 100644 viewer/src/services/indexFactory.ts create mode 100644 viewer/src/services/indexSearch.ts delete mode 100644 viewer/src/services/indexfactory.ts delete mode 100644 viewer/src/services/indexsearch.ts create mode 100644 viewer/src/services/itemComparator.ts delete mode 100644 viewer/src/services/itemComparators.ts create mode 100644 viewer/src/services/itemGuards.ts delete mode 100644 viewer/src/services/ldzoom.ts create mode 100644 viewer/src/services/ui/ldFullscreen.ts create mode 100644 viewer/src/services/ui/ldItemResourceUrl.ts create mode 100644 viewer/src/services/ui/ldKeepFocus.ts create mode 100644 viewer/src/services/ui/ldKeyboard.ts create mode 100644 viewer/src/services/ui/ldSaveScroll.ts create mode 100644 viewer/src/services/ui/ldTitle.ts create mode 100644 viewer/src/services/ui/ldZoom.ts (limited to 'viewer/src/services') diff --git a/viewer/src/services/api/ldFetch.ts b/viewer/src/services/api/ldFetch.ts new file mode 100644 index 0000000..4d2f346 --- /dev/null +++ b/viewer/src/services/api/ldFetch.ts @@ -0,0 +1,35 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2022 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 { MaybeComputedRef, useFetch } from '@vueuse/core'; +import { createToast } from 'mosha-vue-toastify'; + +export const useLdFetch = (url: MaybeComputedRef) => { + const fetchReturn = useFetch(url, { refetch: true }); + fetchReturn.onFetchError((error) => { + createToast(String(error), { + type: 'danger', + position: 'top-center', + timeout: 10000, + showIcon: true, + onClose: fetchReturn.execute, + }); + }); + return fetchReturn; +}; diff --git a/viewer/src/services/dragscrollclickfix.ts b/viewer/src/services/dragscrollclickfix.ts deleted file mode 100644 index 7125510..0000000 --- a/viewer/src/services/dragscrollclickfix.ts +++ /dev/null @@ -1,51 +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/services/fetchWithCheck.ts b/viewer/src/services/fetchWithCheck.ts deleted file mode 100644 index e84e8b6..0000000 --- a/viewer/src/services/fetchWithCheck.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default class FetchWithCheck { - static async get(url: RequestInfo): Promise { - const response = await fetch(url); - if (!response.ok) throw new Error(`${response.status}: ${response.statusText}`); - return response; - } -} diff --git a/viewer/src/services/indexFactory.ts b/viewer/src/services/indexFactory.ts new file mode 100644 index 0000000..a414856 --- /dev/null +++ b/viewer/src/services/indexFactory.ts @@ -0,0 +1,163 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2022 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 { Item, RawTag } from '@/@types/gallery'; +import { Operation } from '@/@types/operation'; +import { TagCategory, TagIndex, TagNode, TagSearch } from '@/@types/tag'; +import { isDirectory } from './itemGuards'; +import { useNavigation } from './navigation'; + +const navigation = useNavigation(); + +function _pushPartToIndex(index: TagNode, part: string, item: Item, rootPart: boolean): TagNode { + if (!index) { + index = { + tag: part, + tagfiltered: navigation.normalize(part), + rootPart, + childPart: !rootPart, + items: [], + children: {}, + }; + } else if (rootPart) index.rootPart = true; + else index.childPart = true; + + if (!index.items.includes(item)) index.items.push(item); + return index; +} + +// Pushes all tags for a root item (and its children) to the index +function _pushTagsForItem(tagsIndex: TagIndex, item: Item): void { + if (isDirectory(item)) { + item.properties.items.forEach(item => _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) { + tagsIndex[part] = _pushPartToIndex(tagsIndex[part], part, item, !lastPart); + if (lastPart) { + const children = tagsIndex[lastPart].children; + children[part] = _pushPartToIndex(children[part], part, item, false); + } + lastPart = part; + } + if (lastPart) tagsIndex[lastPart].childPart = true; + } +} + +function _extractOperation(filter: string): Operation { + const first = filter.slice(0, 1); + switch (first) { + case Operation.ADDITION: + case Operation.SUBSTRACTION: + return first; + default: + return Operation.INTERSECTION; + } +} + +function _searchTagsFromFilterWithCategory( + tagsIndex: TagIndex, + operation: Operation, + category: string, + disambiguation: string, + strict: boolean, +): TagSearch[] { + category = navigation.normalize(category); + disambiguation = navigation.normalize(disambiguation); + return Object.values(tagsIndex) + .filter(node => _matches(node, category, strict)) + .flatMap(node => + Object.values(node.children) + .filter(child => _matches(child, disambiguation, strict)) + .map(child => ({ ...child, parent: node, operation, display: `${operation}${node.tag}:${child.tag}` })), + ); +} + +function _searchTagsFromFilter( + tagsIndex: TagIndex, + operation: Operation, + filter: string, + strict: boolean, +): TagSearch[] { + filter = navigation.normalize(filter); + return Object.values(tagsIndex) + .filter(node => _matches(node, filter, strict)) + .map(node => ({ ...node, operation, display: `${operation}${node.tag}` })); +} + +function _matches(node: TagNode, filter: string, strict: boolean): boolean { + if (strict) return node.tagfiltered === filter; + return node.tagfiltered.includes(filter); +} + +function _isDiscriminantTagOnly(tags: RawTag[], node: TagNode): boolean { + return !tags.includes(node.tag) || !node.childPart; +} + +// --- + +export const useIndexFactory = () => { + function generateTags(root: Item | null): TagIndex { + const tagsIndex: TagIndex = {}; + if (root) _pushTagsForItem(tagsIndex, root); + return tagsIndex; + } + + function searchTags(tagsIndex: TagIndex, filter: string, strict: boolean): TagSearch[] { + let search: TagSearch[] = []; + if (tagsIndex && filter) { + const operation = _extractOperation(filter); + if (operation !== Operation.INTERSECTION) filter = filter.slice(1); + if (filter.includes(':')) { + const filterParts = filter.split(':'); + search = _searchTagsFromFilterWithCategory(tagsIndex, operation, filterParts[0], filterParts[1], strict); + } else { + search = _searchTagsFromFilter(tagsIndex, operation, filter, strict); + } + } + return search; + } + + function generateCategories(tagsIndex: TagIndex, categoryTags?: RawTag[]): TagCategory[] { + if (!categoryTags?.length) return [{ tag: '', index: tagsIndex }]; + + const tagsCategories: TagCategory[] = []; + const tagsRemaining = new Map(Object.entries(tagsIndex)); + categoryTags + .map(tag => ({ tag, index: tagsIndex[tag]?.children })) + .filter(category => category.index && Object.keys(category.index).length) + .forEach(category => { + tagsCategories.push(category); + [category.tag, ...Object.values(category.index).map(node => node.tag)] + .filter(tag => _isDiscriminantTagOnly(categoryTags, tagsIndex[tag])) + .forEach(tag => tagsRemaining.delete(tag)); + }); + tagsCategories.push({ tag: '', index: Object.fromEntries(tagsRemaining) }); + return tagsCategories; + } + + return { + generateTags, + searchTags, + generateCategories, + }; +}; diff --git a/viewer/src/services/indexSearch.ts b/viewer/src/services/indexSearch.ts new file mode 100644 index 0000000..df0a600 --- /dev/null +++ b/viewer/src/services/indexSearch.ts @@ -0,0 +1,74 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2022 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 { Item } from '@/@types/gallery'; +import { Operation } from '@/@types/operation'; +import { TagSearch, TagSearchByOperation } from '@/@types/tag'; + +function _extractTagsByOperation(searchTags: TagSearch[]): TagSearchByOperation { + const byOperation: TagSearchByOperation = {}; + Object.values(Operation).forEach( + operation => (byOperation[operation] = searchTags.filter(tag => tag.operation === operation)), + ); + return byOperation; +} + +function _extractIntersection(byOperation: TagSearchByOperation): Set { + const 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; +} + +function _extractSubstraction(byOperation: TagSearchByOperation): Set { + const substraction = new Set(); + if (byOperation[Operation.SUBSTRACTION].length > 0) { + byOperation[Operation.SUBSTRACTION].flatMap(tag => tag.items).forEach(item => substraction.add(item)); + } + return substraction; +} + +function _aggregateAll( + byOperation: TagSearchByOperation, + intersection: Set, + substraction: Set, +): Item[] { + byOperation[Operation.ADDITION].flatMap(tag => tag.items).forEach(item => intersection.add(item)); + substraction.forEach(item => intersection.delete(item)); + return [...intersection]; +} + +// --- + +export const useIndexSearch = () => { + // Results of the search (by tags) + function indexSearch(searchTags: TagSearch[]): Item[] { + const byOperation = _extractTagsByOperation(searchTags); + const intersection = _extractIntersection(byOperation); + const substraction = _extractSubstraction(byOperation); + return _aggregateAll(byOperation, intersection, substraction); + } + + return indexSearch; +}; diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts deleted file mode 100644 index 691a765..0000000 --- a/viewer/src/services/indexfactory.ts +++ /dev/null @@ -1,157 +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 . -*/ - -import { Item, RawTag } from "@/@types/gallery"; -import { ItemType } from "@/@types/ItemType"; -import { Operation } from "@/@types/Operation"; -import { TagCategory, TagIndex, TagNode, TagSearch } from "@/@types/tag"; -import Navigation from "@/services/navigation"; - -export default class IndexFactory { - public static generateTags(root: Item | null): TagIndex { - const tagsIndex: TagIndex = {}; - 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: TagIndex, item: Item): void { - if (item.properties.type === ItemType.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) { - tagsIndex[part] = IndexFactory.pushPartToIndex(tagsIndex[part], part, item, !Boolean(lastPart)); - if (lastPart) { - const children = tagsIndex[lastPart].children; - children[part] = IndexFactory.pushPartToIndex(children[part], part, item, false); - } - lastPart = part; - } - if (lastPart) tagsIndex[lastPart].childPart = true; - } - } - - private static pushPartToIndex(index: TagNode, part: string, item: Item, rootPart: boolean): TagNode { - if (!index) - index = { - tag: part, - tagfiltered: Navigation.normalize(part), - rootPart, - childPart: !rootPart, - items: [], - children: {}, - }; - else if (rootPart) index.rootPart = true; - else index.childPart = true; - - if (!index.items.includes(item)) index.items.push(item); - return index; - } - - // --- - - public static searchTags(tagsIndex: TagIndex, filter: string, strict: boolean): TagSearch[] { - let search: TagSearch[] = []; - 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], strict); - } else { - search = this.searchTagsFromFilter(tagsIndex, operation, filter, strict); - } - } - 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: TagIndex, - operation: Operation, - category: string, - disambiguation: string, - strict: boolean - ): TagSearch[] { - category = Navigation.normalize(category); - disambiguation = Navigation.normalize(disambiguation); - return Object.values(tagsIndex) - .filter(node => IndexFactory.matches(node, category, strict)) - .flatMap(node => - Object.values(node.children) - .filter(child => IndexFactory.matches(child, disambiguation, strict)) - .map(child => ({ ...child, parent: node, operation, display: `${operation}${node.tag}:${child.tag}` })) - ); - } - - private static searchTagsFromFilter( - tagsIndex: TagIndex, - operation: Operation, - filter: string, - strict: boolean - ): TagSearch[] { - filter = Navigation.normalize(filter); - return Object.values(tagsIndex) - .filter(node => IndexFactory.matches(node, filter, strict)) - .map(node => ({ ...node, operation, display: `${operation}${node.tag}` })); - } - - private static matches(node: TagNode, filter: string, strict: boolean): boolean { - if (strict) return node.tagfiltered === filter; - return node.tagfiltered.includes(filter); - } - - // --- - - public static generateCategories(tagsIndex: TagIndex, categoryTags?: RawTag[]): TagCategory[] { - if (!categoryTags?.length) return [{ tag: "", index: tagsIndex }]; - - const tagsCategories: TagCategory[] = []; - const tagsRemaining = new Map(Object.entries(tagsIndex)); - categoryTags - .map(tag => ({ tag, index: tagsIndex[tag]?.children })) - .filter(category => category.index && Object.keys(category.index).length) - .forEach(category => { - tagsCategories.push(category); - [category.tag, ...Object.values(category.index).map(node => node.tag)] - .filter(tag => IndexFactory.isDiscriminantTagOnly(categoryTags, tagsIndex[tag])) - .forEach(tag => tagsRemaining.delete(tag)); - }); - tagsCategories.push({ tag: "", index: Object.fromEntries(tagsRemaining) }); - return tagsCategories; - } - - private static isDiscriminantTagOnly(tags: RawTag[], node: TagNode): boolean { - return !tags.includes(node.tag) || !node.childPart; - } -} diff --git a/viewer/src/services/indexsearch.ts b/viewer/src/services/indexsearch.ts deleted file mode 100644 index 57bd03c..0000000 --- a/viewer/src/services/indexsearch.ts +++ /dev/null @@ -1,70 +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 . -*/ - -import { Item } from "@/@types/gallery"; -import { Operation } from "@/@types/Operation"; -import { TagSearch, TagSearchByOperation } from "@/@types/tag"; - -export default class IndexSearch { - // Results of the search (by tags) - public static search(searchTags: TagSearch[]): Item[] { - const byOperation = this.extractTagsByOperation(searchTags); - const intersection = this.extractIntersection(byOperation); - const substraction = this.extractSubstraction(byOperation); - return this.aggregateAll(byOperation, intersection, substraction); - } - - private static extractTagsByOperation(searchTags: TagSearch[]): TagSearchByOperation { - const byOperation: TagSearchByOperation = {}; - Object.values(Operation).forEach( - operation => (byOperation[operation] = searchTags.filter(tag => tag.operation === operation)) - ); - return byOperation; - } - - private static extractIntersection(byOperation: TagSearchByOperation): Set { - const 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: TagSearchByOperation): Set { - const 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: TagSearchByOperation, - intersection: Set, - substraction: Set - ): 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/itemComparator.ts b/viewer/src/services/itemComparator.ts new file mode 100644 index 0000000..25010b8 --- /dev/null +++ b/viewer/src/services/itemComparator.ts @@ -0,0 +1,93 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2022 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 { Item, ItemSortStr } from '@/@types/gallery'; +import i18n from '@/plugins/i18n'; +import { isDirectory } from './itemGuards'; + +const { t } = i18n.global; + +export type ItemComparator = (left: Item, right: Item) => number; +export type ItemSort = { name: ItemSortStr, text: string; fn: ItemComparator }; + +function _sortByPathAsc(left: Item, right: Item): number { + return left.path.localeCompare(right.path, undefined, { + sensitivity: 'base', + ignorePunctuation: true, + numeric: true, + }); +} + +function _sortByTitleAsc(left: Item, right: Item): number { + return left.title.localeCompare(right.title, undefined, { + sensitivity: 'base', + ignorePunctuation: true, + numeric: true, + }); +} + +function _sortByDateAsc(left: Item, right: Item): number { + return left.datetime.localeCompare(right.datetime); // TODO: handle timezones +} + +function _sortDirectoryFirst(left: Item, right: Item): number { + const dLeft = isDirectory(left) ? 1 : 0; + const dRight = isDirectory(right) ? 1 : 0; + return dRight - dLeft; +} + +function _reverse(fn: ItemComparator): ItemComparator { + return (l, r) => -fn(l, r); +} + +function _chain(comparators: ItemComparator[]): ItemComparator { + return comparators.reduce((primary, tieBreaker) => (l, r) => { + const primaryComparison = primary(l, r); + return primaryComparison !== 0 ? primaryComparison : tieBreaker(l, r); + }); +} + +// --- + +export const useItemComparator = () => { + const ITEM_SORTS: ItemSort[] = [ + { + name: 'title_asc', + text: t('command.sort.byTitleAsc'), + fn: _chain([_sortDirectoryFirst, _sortByTitleAsc, _sortByPathAsc]), + }, + { + name: 'date_asc', + text: t('command.sort.byDateAsc'), + fn: _chain([_sortDirectoryFirst, _sortByDateAsc, _sortByPathAsc]), + }, + { + name: 'date_desc', + text: t('command.sort.byDateDesc'), + fn: _chain([_sortDirectoryFirst, _reverse(_sortByDateAsc), _sortByPathAsc]), + }, + ]; + + const DEFAULT = ITEM_SORTS[2]; // date_desc + + return { + ITEM_SORTS, + DEFAULT, + }; +}; diff --git a/viewer/src/services/itemComparators.ts b/viewer/src/services/itemComparators.ts deleted file mode 100644 index aceff79..0000000 --- a/viewer/src/services/itemComparators.ts +++ /dev/null @@ -1,74 +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 . -*/ -import { Item, ItemSortStr } from "@/@types/gallery"; -import i18n from "@/plugins/i18n"; -import { TranslateResult } from "vue-i18n"; - -export type ItemComparator = (left: Item, right: Item) => number; -export type ItemSort = { text: TranslateResult; fn: ItemComparator }; - -export default class ItemComparators { - static readonly ITEM_SORTS: Record = { - title_asc: { - text: i18n.t("command.sort.byTitleAsc"), - fn: ItemComparators.chain(ItemComparators.sortByTitleAsc, ItemComparators.sortByPathAsc), - }, - date_asc: { - text: i18n.t("command.sort.byDateAsc"), - fn: ItemComparators.chain(ItemComparators.sortByDateAsc, ItemComparators.sortByPathAsc), - }, - date_desc: { - text: i18n.t("command.sort.byDateDesc"), - fn: ItemComparators.reverse(ItemComparators.chain(ItemComparators.sortByDateAsc, ItemComparators.sortByPathAsc)), - }, - }; - - static readonly DEFAULT = ItemComparators.ITEM_SORTS.date_asc; - - static sortByPathAsc(left: Item, right: Item): number { - return left.path.localeCompare(right.path, undefined, { - sensitivity: "base", - ignorePunctuation: true, - numeric: true, - }); - } - - static sortByTitleAsc(left: Item, right: Item): number { - return left.title.localeCompare(right.title, undefined, { - sensitivity: "base", - ignorePunctuation: true, - numeric: true, - }); - } - - static sortByDateAsc(left: Item, right: Item): number { - return left.datetime.localeCompare(right.datetime); // TODO: handle timezones - } - - static reverse(fn: ItemComparator): ItemComparator { - return (l, r) => -fn(l, r); - } - - static chain(primary: ItemComparator, tieBreaker: ItemComparator): ItemComparator { - return (l, r) => { - const primaryComparison = primary(l, r); - return primaryComparison !== 0 ? primaryComparison : tieBreaker(l, r); - }; - } -} diff --git a/viewer/src/services/itemGuards.ts b/viewer/src/services/itemGuards.ts new file mode 100644 index 0000000..114c2d9 --- /dev/null +++ b/viewer/src/services/itemGuards.ts @@ -0,0 +1,11 @@ +import { DirectoryItem, DownloadableItem, Item } from '@/@types/gallery'; +import { ItemType } from '@/@types/itemType'; + +export function isDirectory(item: Item | null): item is DirectoryItem { + return item?.properties.type === ItemType.DIRECTORY; +} + +export function isDownloadableItem(item: Item | null): item is DownloadableItem { + if (!item?.properties) return false; + return 'resource' in item.properties; +} diff --git a/viewer/src/services/ldzoom.ts b/viewer/src/services/ldzoom.ts deleted file mode 100644 index 33a64c8..0000000 --- a/viewer/src/services/ldzoom.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* ldgallery - A static generator which turns a collection of tagged --- pictures into a searchable web gallery. --- --- Copyright (C) 2020 Pacien TRAN-GIRARD --- --- 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 { PictureProperties, Resolution } from "@/@types/gallery"; -import "hammerjs"; - -/** - * Mousewheel and pinch zoom handler. - */ -export default class LdZoom { - readonly containerElement: HTMLDivElement; - readonly imageElement: HTMLImageElement; - readonly pictureProperties: PictureProperties; - readonly maxScaleFactor: number; - readonly scrollZoomSpeed: number; - scaleFactor: number = 0.0; - - constructor( - containerElement: HTMLDivElement, - imageElement: HTMLImageElement, - pictureProperties: PictureProperties, - maxScaleFactor: number, - scrollZoomSpeed: number - ) { - this.containerElement = containerElement; - this.imageElement = imageElement; - this.pictureProperties = pictureProperties; - this.maxScaleFactor = maxScaleFactor; - this.scrollZoomSpeed = scrollZoomSpeed; - } - - /** - * Register event listeners. - */ - public install() { - this.updateImageScale(this.scaleFactor); - - new ResizeObserver(() => { - this.updateImageScale(this.scaleFactor); - }).observe(this.containerElement); - - this.containerElement.addEventListener("wheel", wheelEvent => { - wheelEvent.preventDefault(); - const scaleFactor = this.scaleFactor - Math.sign(wheelEvent.deltaY) * this.scrollZoomSpeed; - this.zoom(wheelEvent.offsetX, wheelEvent.offsetY, scaleFactor); - }); - - const pinchListener = new Hammer(this.containerElement); - pinchListener.get("pinch").set({ enable: true }); - this.installPinchHandler(pinchListener); - } - - private installPinchHandler(pinchListener: HammerManager) { - let startScaleFactor = 0.0; - - pinchListener.on("pinchstart", (pinchEvent: HammerInput) => { - startScaleFactor = this.scaleFactor; - }); - - pinchListener.on("pinchmove", (pinchEvent: HammerInput) => { - const focusX = pinchEvent.center.x + this.containerElement.scrollLeft; - const focusY = pinchEvent.center.y + this.containerElement.scrollTop; - const scaleFactor = pinchEvent.scale * startScaleFactor; - this.zoom(focusX, focusY, scaleFactor); - }); - } - - /** - * Returns the picture resolution as it should be displayed. - */ - private getDisplayResolution(): Resolution { - return { - width: this.pictureProperties.resolution.width * this.scaleFactor, - height: this.pictureProperties.resolution.height * this.scaleFactor, - }; - } - - /** - * Applies scaling to the DOM image element. - * To call after internal intermediate computations because DOM properties aren't stable. - */ - private resizeImageElement() { - const imageDim = this.getDisplayResolution(); - this.imageElement.width = imageDim.width; - this.imageElement.height = imageDim.height; - } - - /** - * Centers the image element inside its container if it fits, or stick to the top and left borders otherwise. - * It's depressingly hard to do in pure CSS… - */ - private recenterImageElement() { - const imageDim = this.getDisplayResolution(); - const marginLeft = Math.max((this.containerElement.clientWidth - imageDim.width) / 2, 0); - const marginTop = Math.max((this.containerElement.clientHeight - imageDim.height) / 2, 0); - this.imageElement.style.marginLeft = `${marginLeft}px`; - this.imageElement.style.marginTop = `${marginTop}px`; - } - - private zoom(focusX: number, focusY: number, scaleFactor: number) { - const imageDim = this.getDisplayResolution(); - const ratioX = focusX / imageDim.width; - const ratioY = focusY / imageDim.height; - this.updateImageScale(Math.min(scaleFactor, this.maxScaleFactor)); - - const newImageDim = this.getDisplayResolution(); - this.containerElement.scrollLeft -= focusX - ratioX * newImageDim.width; - this.containerElement.scrollTop -= focusY - ratioY * newImageDim.height; - } - - private updateImageScale(newScaleFactor: number) { - const horizontalFillRatio = this.containerElement.clientWidth / this.pictureProperties.resolution.width; - const verticalFillRatio = this.containerElement.clientHeight / this.pictureProperties.resolution.height; - const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0); - this.scaleFactor = Math.max(newScaleFactor, minScaleFactor); - this.resizeImageElement(); - this.recenterImageElement(); - } -} diff --git a/viewer/src/services/navigation.ts b/viewer/src/services/navigation.ts index 5dcea88..b2e807b 100644 --- a/viewer/src/services/navigation.ts +++ b/viewer/src/services/navigation.ts @@ -1,7 +1,7 @@ /* ldgallery - A static generator which turns a collection of tagged -- pictures into a searchable web gallery. -- --- Copyright (C) 2019-2020 Guillaume FOUET +-- Copyright (C) 2019-2022 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 @@ -17,27 +17,31 @@ -- along with this program. If not, see . */ -import { DirectoryItem, Item } from "@/@types/gallery"; -import { ItemType } from "@/@types/ItemType"; +import { DirectoryItem, DownloadableItem, Item } from '@/@types/gallery'; +import { ItemType } from '@/@types/itemType'; +import { faFile, faFileAlt, faFileAudio, faFilePdf, faFileVideo, faFolder, faHome, faImage, IconDefinition } from '@fortawesome/free-solid-svg-icons'; +import { isDirectory } from './itemGuards'; -export default class Navigation { - static readonly ICON_BY_TYPE: Record = { - directory: "folder", - picture: "image", - plaintext: "file-alt", - markdown: "file-alt", - pdf: "file-pdf", - video: "file-video", - audio: "file-audio", - other: "file", - }; +const ICON_BY_TYPE: Record = { + directory: faFolder, + picture: faImage, + plaintext: faFileAlt, + markdown: faFileAlt, + pdf: faFilePdf, + video: faFileVideo, + audio: faFileAudio, + other: faFile, +}; + +// --- +export const useNavigation = () => { // Searches for an item by path from a root item (navigation) - public static searchCurrentItemPath(root: Item, path: string): Item[] { + function searchCurrentItemPath(root: Item, path: string): Item[] { if (path === root.path) return [root]; - if (root.properties.type === ItemType.DIRECTORY && path.startsWith(root.path)) { + if (isDirectory(root) && path.startsWith(root.path)) { const itemChain = root.properties.items - .map(item => this.searchCurrentItemPath(item, path)) + .map(item => searchCurrentItemPath(item, path)) .find(itemChain => itemChain.length > 0); if (itemChain) return [root, ...itemChain]; } @@ -45,47 +49,39 @@ export default class Navigation { } // Normalize a string to lowercase, no-accents - public static normalize(value: string) { + function normalize(value: string) { return value - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') .toLowerCase(); } - // Checks if the type of an item matches - public static checkType(item: Item | null, type: ItemType | null): boolean { - return (item?.properties.type ?? null) === type; - } - - public static getLastDirectory(itemPath: Item[]): DirectoryItem { + function getLastDirectory(itemPath: Item[]): DirectoryItem { for (let idx = itemPath.length - 1; idx >= 0; idx--) { const item = itemPath[idx]; - if (Navigation.checkType(item, ItemType.DIRECTORY)) return item as DirectoryItem; + if (isDirectory(item)) return item; } - throw new Error("No directory found"); - } - - // Sort a list of items, moving the directories to the beginning of the list - public static directoriesFirst(items: Item[]) { - return [ - ...items - .filter(child => Navigation.checkType(child, ItemType.DIRECTORY)) - .sort((a, b) => a.title.localeCompare(b.title)), - - ...items.filter(child => !Navigation.checkType(child, ItemType.DIRECTORY)), - ]; + throw new Error('No directory found'); } // Get the icon for an item - public static getIcon(item: Item): string { - if (item.path.length <= 1) return "home"; - return Navigation.ICON_BY_TYPE[item.properties.type]; + function getIcon(item: Item): IconDefinition { + if (item.path.length <= 1) return faHome; + return ICON_BY_TYPE[item.properties.type]; } // Get the file name of an item, without its cache timestamp - public static getFileName(item: Item): string { - if (item.properties.type === ItemType.DIRECTORY) return item.title; - const timeStamped = item.properties.resource.split("/").pop() ?? ""; - return timeStamped.split("?")[0]; + function getFileName(item: Item): string { + if (isDirectory(item)) return item.title; + const timeStamped = (item as DownloadableItem).properties.resource.split('/').pop() ?? ''; + return timeStamped.split('?')[0]; } -} + + return { + searchCurrentItemPath, + normalize, + getLastDirectory, + getIcon, + getFileName, + }; +}; diff --git a/viewer/src/services/ui/ldFullscreen.ts b/viewer/src/services/ui/ldFullscreen.ts new file mode 100644 index 0000000..80e755d --- /dev/null +++ b/viewer/src/services/ui/ldFullscreen.ts @@ -0,0 +1,41 @@ +/* ldgallery - A static generator which turns a collection of tagged +-- pictures into a searchable web gallery. +-- +-- Copyright (C) 2019-2022 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 { useUiStore } from '@/store/uiStore'; +import { useEventListener } from '@vueuse/core'; +import { watch } from 'vue'; + +export const useLdFullscreen = () => { + const uiStore = useUiStore(); + + useEventListener(document, 'fullscreenchange', onFullscreenChange); + + function onFullscreenChange() { + uiStore.toggleFullscreen(isFullscreenActive()); + } + + function isFullscreenActive(): boolean { + return Boolean(document.fullscreenElement); + } + + watch(() => uiStore.fullscreen, (fullscreen) => { + if (fullscreen && !isFullscreenActive()) document.body.requestFullscreen(); + else if (isFull