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

Merge pull request #43 from koloml/feature/fullscreen-component

Fullscreen Viewer: Moved viewer logic into separate component, added closing with swipe for touch devices, added scroll lock
This commit is contained in:
2024-10-20 03:55:38 +04:00
committed by GitHub
4 changed files with 243 additions and 78 deletions

View File

@@ -0,0 +1,211 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
export class FullscreenViewer extends BaseComponent {
/** @type {HTMLVideoElement} */
#videoElement;
/** @type {HTMLImageElement} */
#imageElement;
/** @type {number|null} */
#touchId = null;
/** @type {number|null} */
#startX = null;
/** @type {number|null} */
#startY = null;
/** @type {boolean|null} */
#isClosingSwipeStarted = null;
/**
* @protected
*/
build() {
this.container.classList.add('fullscreen-viewer');
this.#videoElement = document.createElement('video');
this.#imageElement = document.createElement('img');
}
/**
* @protected
*/
init() {
document.addEventListener('keydown', this.#onDocumentKeyPressed.bind(this));
this.on('click', this.#close.bind(this));
this.on('touchstart', this.#onTouchStart.bind(this));
this.on('touchmove', this.#onTouchMove.bind(this));
this.on('touchend', this.#onTouchEnd.bind(this));
}
/**
* @param {TouchEvent} event
*/
#onTouchStart(event) {
if (this.#touchId !== null) {
return;
}
const firstTouch = event.touches.item(0);
if (!firstTouch) {
return;
}
this.#touchId = firstTouch.identifier;
this.#startX = firstTouch.clientX;
this.#startY = firstTouch.clientY;
this.container.classList.add(FullscreenViewer.#swipeState);
}
/**
* @param {TouchEvent} event
*/
#onTouchEnd(event) {
if (this.#touchId === null) {
return;
}
const endedTouch = Array.from(event.changedTouches)
.find(touch => touch.identifier === this.#touchId);
if (!endedTouch) {
return;
}
const verticalDistance = Math.abs(endedTouch.clientY - this.#startY);
const requiredClosingDistance = window.innerHeight / 3;
if (this.#isClosingSwipeStarted && verticalDistance > requiredClosingDistance) {
this.#close();
}
this.#touchId = null;
this.#startX = null;
this.#startY = null;
this.#isClosingSwipeStarted = null;
this.container.classList.remove(FullscreenViewer.#swipeState);
requestAnimationFrame(() => {
this.container.style.removeProperty(FullscreenViewer.#offsetProperty);
this.container.style.removeProperty(FullscreenViewer.#opacityProperty);
});
}
/**
* @param {TouchEvent} event
*/
#onTouchMove(event) {
if (this.#touchId === null) {
return;
}
if (this.#isClosingSwipeStarted === false) {
return;
}
for (const changedTouch of event.changedTouches) {
if (changedTouch.identifier !== this.#touchId) {
continue;
}
const verticalDistance = changedTouch.clientY - this.#startY;
if (this.#isClosingSwipeStarted === null) {
const horizontalDistance = changedTouch.clientX - this.#startX;
if (Math.abs(verticalDistance) >= FullscreenViewer.#minRequiredDistance) {
this.#isClosingSwipeStarted = true;
} else if (Math.abs(horizontalDistance) >= FullscreenViewer.#minRequiredDistance) {
this.#isClosingSwipeStarted = false;
break;
} else {
break;
}
}
this.container.style.setProperty(
FullscreenViewer.#offsetProperty,
verticalDistance.toString().concat('px')
);
const maxDistance = window.innerHeight * 2;
let opacity = 1;
if (verticalDistance !== 0) {
opacity -= Math.min(1, Math.abs(verticalDistance) / maxDistance);
}
this.container.style.setProperty(
FullscreenViewer.#opacityProperty,
opacity.toString()
);
break;
}
}
/**
* @param {KeyboardEvent} event
*/
#onDocumentKeyPressed(event) {
if (event.code === 'Escape' || event.code === 'Esc') {
this.#close();
}
}
#close() {
this.container.classList.remove(FullscreenViewer.#shownState);
document.body.style.overflow = null;
requestAnimationFrame(() => {
this.#videoElement.volume = 0;
this.#videoElement.pause();
this.#videoElement.remove();
});
}
/**
* @param {string} url
*/
show(url) {
requestAnimationFrame(() => {
this.container.classList.add(FullscreenViewer.#shownState);
document.body.style.overflow = 'hidden';
});
if (FullscreenViewer.#isVideoUrl(url)) {
this.#imageElement.remove();
this.#videoElement.src = url;
this.#videoElement.volume = 0;
this.#videoElement.autoplay = true;
this.#videoElement.loop = true;
this.#videoElement.controls = true;
this.container.append(this.#videoElement);
return;
}
this.#videoElement.remove();
this.#imageElement.src = url;
this.container.append(this.#imageElement);
}
/**
* @param {string} url
* @return {boolean}
*/
static #isVideoUrl(url) {
return url.endsWith('.mp4') || url.endsWith('.webm');
}
static #offsetProperty = '--offset';
static #opacityProperty = '--opacity';
static #shownState = 'shown';
static #swipeState = 'swiped';
static #minRequiredDistance = 50;
}

View File

@@ -1,6 +1,7 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import MiscSettings from "$lib/extension/settings/MiscSettings.js";
import {FullscreenViewer} from "$lib/components/FullscreenViewer.js";
export class ImageShowFullscreenButton extends BaseComponent {
/**
@@ -11,7 +12,6 @@ export class ImageShowFullscreenButton extends BaseComponent {
build() {
this.container.innerText = '🔍';
ImageShowFullscreenButton.#resolveFullscreenViewer();
ImageShowFullscreenButton.#miscSettings ??= new MiscSettings();
}
@@ -45,95 +45,36 @@ export class ImageShowFullscreenButton extends BaseComponent {
}
#onButtonClicked() {
const imageViewer = ImageShowFullscreenButton.#resolveFullscreenViewer();
const largeSourceUrl = this.#mediaBoxTools.mediaBox.imageLinks.large;
let imageElement = imageViewer.querySelector('img');
let videoElement = imageViewer.querySelector('video');
if (imageElement) {
imageElement.remove();
}
if (videoElement) {
videoElement.remove();
}
if (largeSourceUrl.endsWith('.webm') || largeSourceUrl.endsWith('.mp4')) {
videoElement ??= document.createElement('video');
videoElement.src = largeSourceUrl;
videoElement.volume = 0;
videoElement.autoplay = true;
videoElement.loop = true;
videoElement.controls = true;
imageViewer.appendChild(videoElement);
} else {
imageElement ??= document.createElement('img');
imageElement.src = largeSourceUrl;
imageViewer.appendChild(imageElement);
}
imageViewer.classList.add('shown');
ImageShowFullscreenButton
.#resolveViewer()
.show(this.#mediaBoxTools.mediaBox.imageLinks.large);
}
/**
* @type {HTMLElement|null}
* @type {FullscreenViewer|null}
*/
static #fullscreenViewerElement = null;
static #viewer = null;
/**
* @return {HTMLElement}
* @return {FullscreenViewer}
*/
static #resolveFullscreenViewer() {
this.#fullscreenViewerElement ??= this.#buildFullscreenViewer();
return this.#fullscreenViewerElement;
static #resolveViewer() {
this.#viewer ??= this.#buildViewer();
return this.#viewer;
}
/**
* @return {HTMLElement}
* @return {FullscreenViewer}
*/
static #buildFullscreenViewer() {
static #buildViewer() {
const element = document.createElement('div');
element.classList.add('fullscreen-viewer');
const viewer = new FullscreenViewer(element);
viewer.initialize();
document.body.append(element);
document.addEventListener('keydown', event => {
// When ESC pressed
if (event.code === 'Escape' || event.code === 'Esc') {
this.#closeFullscreenViewer(element);
}
});
element.addEventListener('click', () => {
this.#closeFullscreenViewer(element);
});
return element;
}
/**
* @param {HTMLElement} [viewerElement]
*/
static #closeFullscreenViewer(viewerElement = null) {
viewerElement ??= this.#resolveFullscreenViewer();
viewerElement.classList.remove('shown');
/** @type {HTMLVideoElement} */
const videoElement = viewerElement.querySelector('video');
if (!videoElement) {
return;
}
// Stopping and muting the video
requestAnimationFrame(() => {
videoElement.volume = 0;
videoElement.pause();
videoElement.remove();
})
return viewer;
}
/**

View File

@@ -5,10 +5,17 @@ export default class MiscSettings extends CacheableSettings {
super("misc");
}
/**
* @return {Promise<boolean>}
*/
async resolveFullscreenViewerEnabled() {
return this._resolveSetting("fullscreenViewer", true);
}
/**
* @param {boolean} isEnabled
* @return {Promise<void>}
*/
async setFullscreenViewerEnabled(isEnabled) {
return this._writeSetting("fullscreenViewer", isEnabled);
}

View File

@@ -160,9 +160,9 @@
.fullscreen-viewer {
pointer-events: none;
z-index: 9999;
opacity: 0;
opacity: var(--opacity, 0);
background-color: black;
transition: opacity 0.1s;
transition: opacity 0.1s, transform 0.1s;
position: fixed;
left: 0;
right: 0;
@@ -171,6 +171,7 @@
display: flex;
justify-content: stretch;
align-items: stretch;
transform: translateY(var(--offset, 0));
img, video {
object-fit: contain;
@@ -179,7 +180,12 @@
}
&.shown {
opacity: 1;
opacity: var(--opacity, 1);
pointer-events: initial;
}
&.swiped {
opacity: var(--opacity, 1);
transition: none;
}
}