diff --git a/src/app.d.ts b/src/app.d.ts index 4462d62..6040841 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -27,6 +27,13 @@ declare global { profiles: MaintenanceProfile; groups: TagGroup; } + + interface ImageURIs { + full: string; + large: string; + medium: string; + small: string; + } } } diff --git a/src/lib/components/FullscreenViewer.js b/src/lib/components/FullscreenViewer.js index 204f287..ee8b939 100644 --- a/src/lib/components/FullscreenViewer.js +++ b/src/lib/components/FullscreenViewer.js @@ -1,13 +1,14 @@ import {BaseComponent} from "$lib/components/base/BaseComponent.js"; +import MiscSettings from "$lib/extension/settings/MiscSettings.ts"; export class FullscreenViewer extends BaseComponent { /** @type {HTMLVideoElement} */ #videoElement = document.createElement('video'); /** @type {HTMLImageElement} */ #imageElement = document.createElement('img'); - #spinnerElement = document.createElement('i'); - + #sizeSelectorElement = document.createElement('select'); + #closeButtonElement = document.createElement('i'); /** @type {number|null} */ #touchId = null; /** @type {number|null} */ @@ -16,15 +17,33 @@ export class FullscreenViewer extends BaseComponent { #startY = null; /** @type {boolean|null} */ #isClosingSwipeStarted = null; + #isSizeFetched = false; + /** @type {App.ImageURIs|null} */ + #currentURIs = null; /** * @protected */ build() { this.container.classList.add('fullscreen-viewer'); - this.container.append(this.#spinnerElement); + + this.container.append( + this.#spinnerElement, + this.#sizeSelectorElement, + this.#closeButtonElement, + ); this.#spinnerElement.classList.add('spinner', 'fa', 'fa-circle-notch', 'fa-spin'); + this.#closeButtonElement.classList.add('close', 'fa', 'fa-xmark'); + this.#sizeSelectorElement.classList.add('size-selector', 'input'); + + for (const [sizeKey, sizeName] of Object.entries(FullscreenViewer.#previewSizes)) { + const sizeOptionElement = document.createElement('option'); + sizeOptionElement.value = sizeKey; + sizeOptionElement.innerText = sizeName; + + this.#sizeSelectorElement.append(sizeOptionElement); + } } /** @@ -40,6 +59,12 @@ export class FullscreenViewer extends BaseComponent { this.#videoElement.addEventListener('loadeddata', this.#onLoaded.bind(this)); this.#imageElement.addEventListener('load', this.#onLoaded.bind(this)); + this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation()); + + FullscreenViewer.#miscSettings + .resolveFullscreenViewerPreviewSize() + .then(this.#onSizeResolved.bind(this)) + .then(this.#watchForSizeSelectionChanges.bind(this)); } #onLoaded() { @@ -163,7 +188,49 @@ export class FullscreenViewer extends BaseComponent { } } + /** + * @param {import("$lib/extension/settings/MiscSettings.js").FullscreenViewerSize} size + */ + #onSizeResolved(size) { + this.#sizeSelectorElement.value = size; + this.#isSizeFetched = true; + + this.emit('size-loaded'); + } + + #watchForSizeSelectionChanges() { + let lastActiveSize = this.#sizeSelectorElement.value; + + FullscreenViewer.#miscSettings.subscribe(settings => { + const targetSize = settings.fullscreenViewerSize; + + if (!targetSize || lastActiveSize === targetSize) { + return; + } + + lastActiveSize = targetSize; + this.#sizeSelectorElement.value = targetSize; + }); + + this.#sizeSelectorElement.addEventListener('input', () => { + const targetSize = this.#sizeSelectorElement.value; + + if (this.#currentURIs) { + void this.show(this.#currentURIs); + } + + if (!targetSize || targetSize === lastActiveSize || !(targetSize in FullscreenViewer.#previewSizes)) { + return; + } + + lastActiveSize = targetSize; + void FullscreenViewer.#miscSettings.setFullscreenViewerPreviewSize(targetSize); + }); + } + #close() { + this.#currentURIs = null; + this.container.classList.remove(FullscreenViewer.#shownState); document.body.style.overflow = null; @@ -175,9 +242,44 @@ export class FullscreenViewer extends BaseComponent { } /** - * @param {string} url + * @param {App.ImageURIs} imageUris + * @return {Promise} */ - show(url) { + async #resolveCurrentSelectedSizeUrl(imageUris) { + if (!this.#isSizeFetched) { + await new Promise(resolve => this.on('size-loaded', resolve)) + } + + let targetSize = this.#sizeSelectorElement.value; + + if (!imageUris.hasOwnProperty(targetSize)) { + targetSize = FullscreenViewer.#fallbackSize; + } + + if (!imageUris.hasOwnProperty(targetSize)) { + targetSize = Object.keys(imageUris)[0]; + } + + if (!targetSize) { + return null; + } + + return imageUris[targetSize]; + } + + /** + * @param {App.ImageURIs} imageUris + */ + async show(imageUris) { + this.#currentURIs = imageUris; + + const url = await this.#resolveCurrentSelectedSizeUrl(imageUris); + + if (!url) { + console.warn('Failed to resolve media for the viewer!'); + return; + } + this.container.classList.add('loading'); requestAnimationFrame(() => { @@ -214,9 +316,23 @@ export class FullscreenViewer extends BaseComponent { return url.endsWith('.mp4') || url.endsWith('.webm'); } + static #miscSettings = new MiscSettings(); + static #offsetProperty = '--offset'; static #opacityProperty = '--opacity'; static #shownState = 'shown'; static #swipeState = 'swiped'; static #minRequiredDistance = 50; + + /** + * @type {Record} + */ + static #previewSizes = { + full: 'Full', + large: 'Large', + medium: 'Medium', + small: 'Small' + } + + static #fallbackSize = 'large'; } diff --git a/src/lib/components/ImageShowFullscreenButton.js b/src/lib/components/ImageShowFullscreenButton.js index f0cd8c7..2f8c6e3 100644 --- a/src/lib/components/ImageShowFullscreenButton.js +++ b/src/lib/components/ImageShowFullscreenButton.js @@ -33,7 +33,7 @@ export class ImageShowFullscreenButton extends BaseComponent { }) .then(() => { ImageShowFullscreenButton.#miscSettings.subscribe(settings => { - this.#isFullscreenButtonEnabled = settings.fullscreenViewer; + this.#isFullscreenButtonEnabled = settings.fullscreenViewer ?? true; this.#updateFullscreenButtonVisibility(); }) }) @@ -47,7 +47,7 @@ export class ImageShowFullscreenButton extends BaseComponent { #onButtonClicked() { ImageShowFullscreenButton .#resolveViewer() - .show(this.#mediaBoxTools.mediaBox.imageLinks.large); + .show(this.#mediaBoxTools.mediaBox.imageLinks); } /** diff --git a/src/lib/components/MediaBoxWrapper.js b/src/lib/components/MediaBoxWrapper.js index cc4c6a2..5fb1a3f 100644 --- a/src/lib/components/MediaBoxWrapper.js +++ b/src/lib/components/MediaBoxWrapper.js @@ -56,7 +56,7 @@ export class MediaBoxWrapper extends BaseComponent { } /** - * @return {ImageURIs} + * @return {App.ImageURIs} */ get imageLinks() { return JSON.parse(this.#thumbnailContainer.dataset.uris); @@ -100,10 +100,3 @@ export function calculateMediaBoxesPositions(mediaBoxesList) { } }) } - -/** - * @typedef {Object} ImageURIs - * @property {string} full - * @property {string} large - * @property {string} small - */ diff --git a/src/lib/extension/settings/MiscSettings.ts b/src/lib/extension/settings/MiscSettings.ts index 94ea7d2..1e0e76d 100644 --- a/src/lib/extension/settings/MiscSettings.ts +++ b/src/lib/extension/settings/MiscSettings.ts @@ -1,7 +1,10 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings.ts"; +export type FullscreenViewerSize = 'small' | 'medium' | 'large' | 'full'; + interface MiscSettingsFields { fullscreenViewer: boolean; + fullscreenViewerSize: FullscreenViewerSize; } export default class MiscSettings extends CacheableSettings { @@ -13,7 +16,15 @@ export default class MiscSettings extends CacheableSettings return this._resolveSetting("fullscreenViewer", true); } + async resolveFullscreenViewerPreviewSize() { + return this._resolveSetting('fullscreenViewerSize', 'large'); + } + async setFullscreenViewerEnabled(isEnabled: boolean) { return this._writeSetting("fullscreenViewer", isEnabled); } + + async setFullscreenViewerPreviewSize(size: FullscreenViewerSize | string) { + return this._writeSetting('fullscreenViewerSize', size as FullscreenViewerSize); + } } diff --git a/src/styles/content/listing.scss b/src/styles/content/listing.scss index 37804f5..a6ec9e9 100644 --- a/src/styles/content/listing.scss +++ b/src/styles/content/listing.scss @@ -199,6 +199,30 @@ transition: opacity .25s ease; } + .size-selector { + position: absolute; + top: 5px; + left: 5px; + z-index: 1; + } + + .close { + position: absolute; + top: 5px; + right: 5px; + z-index: 1; + padding: 5px; + background-color: colors.$text; + color: colors.$background; + font-size: 20px; + line-height: 20px; + width: 20px; + height: 20px; + text-align: center; + display: block; + cursor: pointer; + } + &.shown { opacity: var(--opacity, 1); pointer-events: initial;