Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 32 additions & 37 deletions src/components/slideshow/slideshow.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import dom from '../../utils/dom';
import './style.scss';
import 'material-design-icons-iconfont';
import '../../elements/emby-button/paper-icon-button-light';
import { getIcon } from './slideshowhelper';
import ZoomControl from './zoomControl';

/**
* Name of transition event.
Expand Down Expand Up @@ -106,20 +108,6 @@ function getImgUrl(item, user) {
}
}

/**
* Generates a button using the specified icon, classes and properties.
* @param {string} icon - Name of the material icon on the button
* @param {string} cssClass - CSS classes to assign to the button
* @param {boolean} canFocus - Flag to set the tabindex attribute on the button to -1.
* @param {boolean} autoFocus - Flag to set the autofocus attribute on the button.
* @returns {string} The HTML markup of the button.
*/
function getIcon(icon, cssClass, canFocus, autoFocus) {
const tabIndex = canFocus ? '' : ' tabindex="-1"';
autoFocus = autoFocus ? ' autofocus' : '';
return '<button is="paper-icon-button-light" class="autoSize ' + cssClass + '"' + tabIndex + autoFocus + '><span class="material-icons slideshowButtonIcon ' + icon + '" aria-hidden="true"></span></button>';
}

/**
* Sets the viewport meta tag to enable or disable scaling by the user.
* @param {boolean} scalable - Flag to set the scalability of the viewport.
Expand All @@ -144,6 +132,8 @@ export default function (options) {
let hideTimeout;
/** Last coordinates of the mouse pointer. */
let lastMouseMoveData;
/** @type {ZoomControl|undefined} Instance of zoom control */
let zoomControl;

/**
* Creates the HTML markup for the dialog and the OSD.
Expand Down Expand Up @@ -193,18 +183,23 @@ export default function (options) {

if (!actionButtonsOnTop) {
html += '<div class="slideshowBottomBar hide">';

html += '<div class="slideshowBottomBarLeft">';
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true, true);
if (appHost.supports(AppFeature.FileDownload) && slideshowOptions?.user.Policy.EnableContentDownloading) {
html += getIcon('file_download', 'btnDownload slideshowButton', true);
}
if (appHost.supports(AppFeature.Sharing)) {
html += getIcon('share', 'btnShare slideshowButton', true);
}
html += '</div>';

html += '<div class="slideshowBottomBarRight">';
html += ZoomControl.getHtml();
if (screenfull.isEnabled) {
html += getIcon('fullscreen', 'btnFullscreen', true);
html += getIcon('fullscreen_exit', 'btnFullscreenExit hide', true);
}
html += '</div>';

html += '</div>';
}
Expand Down Expand Up @@ -306,31 +301,26 @@ export default function (options) {
const zoomImage = slideEl.querySelector('.swiper-zoom-fakeimg');

if (zoomImage) {
zoomImage.classList.add('swiper-zoom-fakeimg-hidden');
zoomImage.style.width = zoomImage.style.height = scale * 100 + '%';

if (scale > 1) {
if (zoomImage.classList.contains('swiper-zoom-fakeimg-hidden')) {
// Await for Swiper style changes
setTimeout(() => {
const callback = () => {
imageEl.removeEventListener(transitionEndEventName, callback);
zoomImage.classList.remove('swiper-zoom-fakeimg-hidden');
};

// Swiper set 'transition-duration: 300ms' for auto zoom
// and 'transition-duration: 0s' for touch zoom
const transitionDuration = parseFloat(imageEl.style.transitionDuration.replace(/[a-z]/i, ''));

if (transitionDuration > 0) {
imageEl.addEventListener(transitionEndEventName, callback);
} else {
callback();
}
}, 0);
// Await for Swiper style changes
setTimeout(() => {
const callback = () => {
imageEl.removeEventListener(transitionEndEventName, callback);
zoomImage.classList.remove('swiper-zoom-fakeimg-hidden');
};

// Swiper set 'transition-duration: 300ms' for auto zoom
// and 'transition-duration: 0s' for touch zoom
const transitionDuration = parseFloat(imageEl.style.transitionDuration.replace(/[a-z]/i, ''));

if (transitionDuration > 0) {
imageEl.addEventListener(transitionEndEventName, callback);
} else {
callback();
}
} else {
zoomImage.classList.add('swiper-zoom-fakeimg-hidden');
}
}, 0);
}
}

Expand All @@ -353,6 +343,7 @@ export default function (options) {
// eslint-disable-next-line import/no-unresolved
import('swiper/bundle').then(({ Swiper }) => {
swiperInstance = new Swiper(dialogElement.querySelector('.slideshowSwiperContainer'), {
init: false,
direction: 'horizontal',
// Loop is disabled due to the virtual slides option not supporting it.
loop: false,
Expand Down Expand Up @@ -390,6 +381,10 @@ export default function (options) {
swiperInstance.on('zoomChange', onZoomChange);
}

zoomControl = new ZoomControl(dialogElement, swiperInstance, slides);
zoomControl.bindEvents();
swiperInstance.init();

if (swiperInstance.autoplay?.running) onAutoplayStart();
});
}
Expand Down
15 changes: 15 additions & 0 deletions src/components/slideshow/slideshowhelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Generates a button using the specified icon, classes and properties.
* @param icon - Name of the material icon on the button
* @param cssClass - CSS classes to assign to the button
* @param canFocus - Flag to set the tabindex attribute on the button to -1.
* @param autoFocusFlag - Flag to set the autofocus attribute on the button.
* @returns The HTML markup of the button.
*/
export function getIcon(icon: string, cssClass: string, canFocus?: boolean, autoFocusFlag?: boolean) {
const tabIndex = canFocus ? '' : ' tabindex="-1"';
const autoFocus = autoFocusFlag ? ' autofocus' : '';
return '<button is="paper-icon-button-light" class="autoSize ' + cssClass + '"'
+ tabIndex + autoFocus + '><span class="material-icons slideshowButtonIcon '
+ icon + '" aria-hidden="true"></span></button>';
}
14 changes: 13 additions & 1 deletion src/components/slideshow/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
justify-content: space-between;
}

.slideshowBottomBarLeft,
.slideshowBottomBarRight {
display: flex;
flex-direction: row;
align-items: center;
}

.slideshowTopBar {
Expand Down Expand Up @@ -140,3 +147,8 @@
.swiper-zoom-fakeimg-hidden {
display: none;
}

.zoomControl {
display: flex;
align-items: center;
}
109 changes: 109 additions & 0 deletions src/components/slideshow/zoomControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type Swiper from 'swiper';
import { getIcon } from './slideshowhelper';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';

const buttonZoomStep = 0.5;
const minZoom = 0.1;
const maxZoom = 3;

export default class ZoomControl {
swiper: Swiper;
dialog: HTMLDivElement;
slides: BaseItemDto[];

static getHtml() {
return `\
<div class="zoomControl">\
${getIcon('zoom_out', 'btnZoomOut', true)}\
<div class="sliderContainer"><input is="emby-slider" type="range" step="0.001" min="${minZoom}" max="${maxZoom}" value="1" class="zoomSlider"/></div>\
${getIcon('zoom_in', 'btnZoomIn', true)}\
</div>\
`;
}

constructor(dialog: HTMLDivElement, swiper: Swiper, slides: BaseItemDto[]) {
this.dialog = dialog;
this.swiper = swiper;
this.slides = slides;
}

bindEvents() {
const zoomSlider = this.dialog.querySelector<HTMLInputElement>('.zoomSlider');
if (zoomSlider) {
zoomSlider.addEventListener('input', (e) => {
const initialRealScale = this.getInitialRealScale();
if (e.target && initialRealScale) {
const realScale = Number((e.target as HTMLInputElement).value);
this.swiper.zoom.in(realScale / initialRealScale);
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(zoomSlider as any).getBubbleText = (_: number, value: number) => `${Math.trunc(value * 100)}%`;
}
this.dialog.querySelector<HTMLButtonElement>('.btnZoomOut')?.addEventListener('click', () => {
const initialRealScale = this.getInitialRealScale();
if (initialRealScale) {
this.swiper.zoom.in(Math.max(this.swiper.zoom.scale - buttonZoomStep, Math.min(initialRealScale, minZoom) / initialRealScale));
}
});
this.dialog.querySelector<HTMLButtonElement>('.btnZoomIn')?.addEventListener('click', () => {
const initialRealScale = this.getInitialRealScale();
if (initialRealScale) {
this.swiper.zoom.in(Math.min(this.swiper.zoom.scale + buttonZoomStep, Math.max(initialRealScale, maxZoom) / initialRealScale));
}
});

this.swiper.on('zoomChange', (_: Swiper, scale: number) => {
this.updateZoomControl(scale);
});

this.swiper.on('slidesUpdated', () => {
this.updateZoomControl();
});
}

private getCurrentImageElement() {
const currentItemId = this.slides[this.swiper.activeIndex].Id;
const activeSlide = this.dialog.querySelector(`[data-itemid="${currentItemId}"]`);
return activeSlide?.querySelector<HTMLImageElement>('img');
}

private getInitialRealScale() {
const imageElement = this.getCurrentImageElement();
if (imageElement) {
return imageElement.width / imageElement.naturalWidth;
}
}

private updateZoomControl(scale?: number) {
const updateZoomControlAfterImageLoaded = () => {
const initialRealScale = this.getInitialRealScale();
if (initialRealScale) {
const currentRealScale = (scale ?? this.swiper.zoom.scale) * initialRealScale;
const zoomOutButton = this.dialog.querySelector<HTMLButtonElement>('.btnZoomOut');
if (zoomOutButton) {
zoomOutButton.disabled = currentRealScale <= Math.min(initialRealScale, minZoom);
}
const zoomInButton = this.dialog.querySelector<HTMLButtonElement>('.btnZoomIn');
if (zoomInButton) {
zoomInButton.disabled = currentRealScale >= Math.max(initialRealScale, maxZoom);
}
const zoomSlider = this.dialog.querySelector<HTMLInputElement>('.zoomSlider');
if (zoomSlider) {
zoomSlider.max = Math.max(initialRealScale, maxZoom).toFixed(3);
zoomSlider.min = Math.min(initialRealScale, minZoom).toFixed(3);
zoomSlider.value = currentRealScale.toString();
}
}
};

const imageElement = this.getCurrentImageElement();
if (imageElement) {
if (!imageElement.complete) {
imageElement.onload = updateZoomControlAfterImageLoaded;
} else {
updateZoomControlAfterImageLoaded();
}
}
}
}