1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-23 23:02:58 +00:00

Merge pull request #70 from koloml/feature/fullscreen-controls

Fullscreen Viewer: Added image/video quality setting and more explicit "close" icon
This commit is contained in:
2025-01-03 06:41:41 +04:00
committed by GitHub
6 changed files with 166 additions and 15 deletions

7
src/app.d.ts vendored
View File

@@ -27,6 +27,13 @@ declare global {
profiles: MaintenanceProfile;
groups: TagGroup;
}
interface ImageURIs {
full: string;
large: string;
medium: string;
small: string;
}
}
}

View File

@@ -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<string|null>}
*/
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<import("$lib/extension/settings/MiscSettings.js").FullscreenViewerSize, string>}
*/
static #previewSizes = {
full: 'Full',
large: 'Large',
medium: 'Medium',
small: 'Small'
}
static #fallbackSize = 'large';
}

View File

@@ -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);
}
/**

View File

@@ -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
*/

View File

@@ -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<MiscSettingsFields> {
@@ -13,7 +16,15 @@ export default class MiscSettings extends CacheableSettings<MiscSettingsFields>
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);
}
}

View File

@@ -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;