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