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/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 ++++++++++++++++++++++++++++ 7 files changed, 317 insertions(+) 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/ui') 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 (isFullscreenActive()) document.exitFullscreen(); + }); +}; diff --git a/viewer/src/services/ui/ldItemResourceUrl.ts b/viewer/src/services/ui/ldItemResourceUrl.ts new file mode 100644 index 0000000..7db7ab9 --- /dev/null +++ b/viewer/src/services/ui/ldItemResourceUrl.ts @@ -0,0 +1,15 @@ +import { Item } from '@/@types/gallery'; +import { useGalleryStore } from '@/store/galleryStore'; +import { computed } from 'vue'; +import { isDownloadableItem } from '../itemGuards'; + +export const useItemResource = (item: Item) => { + const galleryStore = useGalleryStore(); + const itemResourceUrl = computed(() => isDownloadableItem(item) ? galleryStore.resourceRoot + item.properties.resource : ''); + const thumbnailResourceUrl = computed(() => item.thumbnail ? galleryStore.resourceRoot + item.thumbnail.resource : ''); + + return { + itemResourceUrl, + thumbnailResourceUrl, + }; +}; diff --git a/viewer/src/services/ui/ldKeepFocus.ts b/viewer/src/services/ui/ldKeepFocus.ts new file mode 100644 index 0000000..ce9cbc8 --- /dev/null +++ b/viewer/src/services/ui/ldKeepFocus.ts @@ -0,0 +1,34 @@ +/* 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 { MaybeElementRef, useFocus } from '@vueuse/core'; +import { watch } from 'vue'; +import { useRoute } from 'vue-router'; + +export const useLdKeepFocus = (element: MaybeElementRef) => { + const contentFocus = useFocus(element); + const route = useRoute(); + + watch(() => route.path, moveFocus); + + function moveFocus() { + contentFocus.focused.value = true; + } + return { moveFocus }; +}; diff --git a/viewer/src/services/ui/ldKeyboard.ts b/viewer/src/services/ui/ldKeyboard.ts new file mode 100644 index 0000000..8576435 --- /dev/null +++ b/viewer/src/services/ui/ldKeyboard.ts @@ -0,0 +1,28 @@ +/* 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 { onKeyStroke } from '@vueuse/core'; + +export const useLdKeyboard = () => { + const uiStore = useUiStore(); + + // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values + onKeyStroke('Escape', () => uiStore.toggleFullscreen(false)); +}; diff --git a/viewer/src/services/ui/ldSaveScroll.ts b/viewer/src/services/ui/ldSaveScroll.ts new file mode 100644 index 0000000..34e277b --- /dev/null +++ b/viewer/src/services/ui/ldSaveScroll.ts @@ -0,0 +1,37 @@ +/* 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 { MaybeElementRef, unrefElement } from '@vueuse/core'; +import { nextTick, watch } from 'vue'; +import { useRoute } from 'vue-router'; + +type ScrollPosition = Record; + +export const useLdSaveScroll = (element: MaybeElementRef) => { + const scrollPositions: ScrollPosition = {}; + const route = useRoute(); + + watch(() => decodeURIComponent(route.path), (newRoute, oldRoute) => { + const el = unrefElement(element); + if (!el) return; + + scrollPositions[oldRoute] = el.scrollTop / el.scrollHeight; + nextTick(() => (el.scrollTop = scrollPositions[newRoute] * el.scrollHeight)); + }); +}; diff --git a/viewer/src/services/ui/ldTitle.ts b/viewer/src/services/ui/ldTitle.ts new file mode 100644 index 0000000..df80140 --- /dev/null +++ b/viewer/src/services/ui/ldTitle.ts @@ -0,0 +1,34 @@ +/* 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 { useGalleryStore } from '@/store/galleryStore'; +import { useTitle } from '@vueuse/core'; +import { computed } from 'vue'; + +export const useLdTitle = () => { + const galleryStore = useGalleryStore(); + + const title = computed(() => { + const { currentItem, galleryTitle } = galleryStore; + return currentItem?.title + ? `${currentItem.title} • ${galleryTitle}` + : galleryTitle; + }); + useTitle(title); +}; diff --git a/viewer/src/services/ui/ldZoom.ts b/viewer/src/services/ui/ldZoom.ts new file mode 100644 index 0000000..9f77dea --- /dev/null +++ b/viewer/src/services/ui/ldZoom.ts @@ -0,0 +1,128 @@ +/* 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 { importHammer } from '@/plugins/asyncLib'; +import { CSSProperties, Ref } from 'vue'; + +/** + * Mousewheel and pinch zoom handler. + */ +export const useLdZoom = ( + imageStyle: Ref, + containerElement: HTMLDivElement, + imageElement: HTMLImageElement, + pictureProperties: PictureProperties, + maxScaleFactor = 10, + scrollZoomSpeed: number = 1 / 7, +) => { + let scaleFactor = 0.0; + + /** + * Register event listeners. + */ + updateImageScale(scaleFactor); + + new ResizeObserver(() => { + updateImageScale(scaleFactor); + }).observe(containerElement); + + containerElement.addEventListener('wheel', wheelEvent => { + wheelEvent.preventDefault(); + const newScaleFactor = scaleFactor - Math.sign(wheelEvent.deltaY) * (scrollZoomSpeed * scaleFactor); + zoom(wheelEvent.offsetX, wheelEvent.offsetY, newScaleFactor); + }); + + importHammer().then(() => { + const pinchListener = new Hammer(containerElement); + pinchListener.get('pinch').set({ enable: true }); + installPinchHandler(pinchListener); + }); + + return { imageStyle }; + + // --- + + function installPinchHandler(pinchListener: HammerManager) { + let startScaleFactor = 0.0; + + pinchListener.on('pinchstart', () => { + startScaleFactor = scaleFactor; + }); + + pinchListener.on('pinchmove', (pinchEvent: HammerInput) => { + const focusX = pinchEvent.center.x + containerElement.scrollLeft; + const focusY = pinchEvent.center.y + containerElement.scrollTop; + const scaleFactor = pinchEvent.scale * startScaleFactor; + zoom(focusX, focusY, scaleFactor); + }); + } + + /** + * Returns the picture resolution as it should be displayed. + */ + function getDisplayResolution(): Resolution { + return { + width: pictureProperties.resolution.width * scaleFactor, + height: pictureProperties.resolution.height * scaleFactor, + }; + } + + /** + * Applies scaling to the DOM image element. + * To call after internal intermediate computations because DOM properties aren't stable. + */ + function resizeImageElement() { + const imageDim = getDisplayResolution(); + imageElement.width = imageDim.width; + 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… + */ + function recenterImageElement() { + const imageDim = getDisplayResolution(); + const marginLeft = Math.max((containerElement.clientWidth - imageDim.width) / 2, 0); + const marginTop = Math.max((containerElement.clientHeight - imageDim.height) / 2, 0); + imageStyle.value.marginLeft = `${marginLeft}px`; + imageStyle.value.marginTop = `${marginTop}px`; + } + + function zoom(focusX: number, focusY: number, scaleFactor: number) { + const imageDim = getDisplayResolution(); + const ratioX = focusX / imageDim.width; + const ratioY = focusY / imageDim.height; + updateImageScale(Math.min(scaleFactor, maxScaleFactor)); + + const newImageDim = getDisplayResolution(); + containerElement.scrollLeft -= focusX - ratioX * newImageDim.width; + containerElement.scrollTop -= focusY - ratioY * newImageDim.height; + } + + function updateImageScale(newScaleFactor: number) { + const horizontalFillRatio = containerElement.clientWidth / pictureProperties.resolution.width; + const verticalFillRatio = containerElement.clientHeight / pictureProperties.resolution.height; + const minScaleFactor = Math.min(horizontalFillRatio, verticalFillRatio, 1.0); + scaleFactor = Math.max(newScaleFactor, minScaleFactor); + resizeImageElement(); + recenterImageElement(); + } +}; -- cgit v1.2.3