Skip to content

Commit

Permalink
Move to PlaybackSubscriber
Browse files Browse the repository at this point in the history
  • Loading branch information
viown committed Oct 14, 2024
1 parent eb5f65e commit 7267b66
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 133 deletions.
13 changes: 8 additions & 5 deletions src/components/playback/playbackmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { MediaError } from 'types/mediaError';
import { getMediaError } from 'utils/mediaError';
import { toApi } from 'utils/jellyfin-apiclient/compat';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
import browser from 'scripts/browser.js';
import { bindSkipSegment } from './skipsegment.ts';

const UNLIMITED_ITEMS = -1;

Expand Down Expand Up @@ -933,11 +935,9 @@ export class PlaybackManager {
return Promise.resolve(self._playQueueManager.getPlaylist());
};

self.promptToSkip = function (mediaSegment, player) {
player = player || self._currentPlayer;

if (player && mediaSegment) {
Events.trigger(player, 'promptskip', [mediaSegment]);
self.promptToSkip = function (mediaSegment) {
if (mediaSegment && this._skipSegment) {
this._skipSegment.onPromptSkip(mediaSegment);
}
};

Expand Down Expand Up @@ -3674,6 +3674,9 @@ export class PlaybackManager {
}

bindMediaSegmentManager(self);
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
this._skipSegment = bindSkipSegment(self);
}
}

getCurrentPlayer() {
Expand Down
240 changes: 113 additions & 127 deletions src/components/playback/skipsegment.ts
Original file line number Diff line number Diff line change
@@ -1,176 +1,162 @@
import { EventType } from 'types/eventType';
import { playbackManager } from './playbackmanager';
import { PlaybackManager } from './playbackmanager';
import { TICKS_PER_MILLISECOND } from 'constants/time';
import Events, { type Event } from '../../utils/events';
import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments';
import './skipbutton.scss';
import dom from 'scripts/dom';
import globalize from 'lib/globalize';
import { Plugin } from 'types/plugin';
import { MediaSegmentDto, MediaSegmentType } from '@jellyfin/sdk/lib/generated-client';
import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber';

interface ShowOptions {
animate?: boolean;
keep?: boolean;
}

let currentPlayer: Plugin | null;
let skipElement: HTMLButtonElement | null;
let currentSegment: MediaSegmentDto | null;
let hideTimeout: ReturnType<typeof setTimeout> | null;

function createSkipElement() {
if (!skipElement && currentSegment) {
const elem = document.createElement('button');
elem.classList.add('skip-button');
elem.classList.add('hide');
elem.classList.add('skip-button-hidden');

elem.addEventListener('click', () => {
if (currentSegment) {
playbackManager.seek(currentSegment.EndTicks);
}
});
class SkipSegment extends PlaybackSubscriber {
private skipElement: HTMLButtonElement | undefined;
private currentSegment: MediaSegmentDto | null | undefined;
private hideTimeout: ReturnType<typeof setTimeout> | null | undefined;

document.body.appendChild(elem);
skipElement = elem;
}
}
constructor(playbackManager: PlaybackManager) {
super(playbackManager);

function setButtonText() {
if (skipElement && currentSegment) {
if (currentPlayer && currentSegment.EndTicks
&& currentSegment.Type === MediaSegmentType.Outro
&& currentSegment.EndTicks >= playbackManager.currentItem(currentPlayer).RunTimeTicks
&& playbackManager.getNextItem()
) {
// Display "Next Episode" if it's an outro segment, exceeds or is equal to the runtime, and if there is a next track.
skipElement.innerHTML += globalize.translate('MediaSegmentNextEpisode');
} else {
skipElement.innerHTML = globalize.translate('MediaSegmentSkipPrompt', globalize.translate(`MediaSegmentType.${currentSegment.Type}`));
}
skipElement.innerHTML += '<span class="material-icons skip_next" aria-hidden="true"></span>';
}
}
this.onOsdChanged = this.onOsdChanged.bind(this);

function clearHideTimeout() {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
Events.on(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged);
}
}

function onHideComplete() {
if (skipElement) {
skipElement.classList.add('hide');
onOsdChanged(_e: Event, isOpen: boolean) {
if (this.currentSegment) {
if (isOpen) {
this.showSkipButton({
animate: false,
keep: true
});
} else if (!this.hideTimeout) {
this.hideSkipButton();
}
}
}
}

function showSkipButton(options: ShowOptions) {
const elem = skipElement;
if (elem) {
clearHideTimeout();
dom.removeEventListener(elem, dom.whichTransitionEvent(), onHideComplete, {
once: true
});
elem.classList.remove('hide');
if (!options.animate) {
elem.classList.add('no-transition');
} else {
elem.classList.remove('no-transition');
onHideComplete() {
if (this.skipElement) {
this.skipElement.classList.add('hide');
}
}

void elem.offsetWidth;
createSkipElement() {
if (!this.skipElement && this.currentSegment) {
const elem = document.createElement('button');
elem.classList.add('skip-button');
elem.classList.add('hide');
elem.classList.add('skip-button-hidden');

requestAnimationFrame(() => {
elem.classList.remove('skip-button-hidden');
elem.addEventListener('click', () => {
if (this.currentSegment) {
this.playbackManager.seek(this.currentSegment.EndTicks);
}
});

if (!options.keep) {
hideTimeout = setTimeout(hideSkipButton, 6000);
}
});
document.body.appendChild(elem);
this.skipElement = elem;
}
}
}

function hideSkipButton() {
const elem = skipElement;
if (elem) {
elem.classList.remove('no-transition');
void elem.offsetWidth;

requestAnimationFrame(function() {
elem.classList.add('skip-button-hidden');
setButtonText() {
if (this.skipElement && this.currentSegment) {
if (this.player && this.currentSegment.EndTicks
&& this.currentSegment.Type === MediaSegmentType.Outro
&& this.currentSegment.EndTicks >= this.playbackManager.currentItem(this.player).RunTimeTicks
&& this.playbackManager.getNextItem()
) {
// Display "Next Episode" if it's an outro segment, exceeds or is equal to the runtime, and if there is a next track.
this.skipElement.innerHTML += globalize.translate('MediaSegmentNextEpisode');
} else {
this.skipElement.innerHTML = globalize.translate('MediaSegmentSkipPrompt', globalize.translate(`MediaSegmentType.${this.currentSegment.Type}`));
}
this.skipElement.innerHTML += '<span class="material-icons skip_next" aria-hidden="true"></span>';
}
}

dom.addEventListener(elem, dom.whichTransitionEvent(), onHideComplete, {
showSkipButton(options: ShowOptions) {
const elem = this.skipElement;
if (elem) {
this.clearHideTimeout();
dom.removeEventListener(elem, dom.whichTransitionEvent(), this.onHideComplete, {
once: true
});
});
}
}

function onPromptSkip(_e: Event, segment: MediaSegmentDto) {
if (!currentSegment) {
currentSegment = segment;
elem.classList.remove('hide');
if (!options.animate) {
elem.classList.add('no-transition');
} else {
elem.classList.remove('no-transition');
}

createSkipElement();
void elem.offsetWidth;

setButtonText();
requestAnimationFrame(() => {
elem.classList.remove('skip-button-hidden');

showSkipButton({ animate: true });
if (!options.keep) {
this.hideTimeout = setTimeout(this.hideSkipButton.bind(this), 6000);
}
});
}
}
}

function onOsdChanged(_e: Event, isOpen: boolean) {
if (currentSegment) {
if (isOpen) {
showSkipButton({
animate: false,
keep: true
hideSkipButton() {
const elem = this.skipElement;
if (elem) {
elem.classList.remove('no-transition');
void elem.offsetWidth;

requestAnimationFrame(() => {
elem.classList.add('skip-button-hidden');

dom.addEventListener(elem, dom.whichTransitionEvent(), this.onHideComplete, {
once: true
});
});
} else if (!hideTimeout) {
hideSkipButton();
}
}
}

function onTimeUpdate() {
if (currentSegment) {
const time = playbackManager.currentTime(currentPlayer) * TICKS_PER_MILLISECOND;

if (!isInSegment(currentSegment, time)) {
currentSegment = null;
hideSkipButton();
clearHideTimeout() {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
}
}

function cleanupPlayer() {
Events.off(currentPlayer, 'promptskip', onPromptSkip);
Events.off(document, EventType.SHOW_VIDEO_OSD, onOsdChanged);
Events.off(currentPlayer, 'playbackstop', onPlaybackStopped);
Events.off(currentPlayer, 'timeupdate', onTimeUpdate);
}
onPromptSkip(segment: MediaSegmentDto) {
if (!this.currentSegment) {
this.currentSegment = segment;

function onPlaybackStopped() {
currentSegment = null;
hideSkipButton();
cleanupPlayer();
}
this.createSkipElement();

this.setButtonText();

function bindToPlayer(player: Plugin) {
if (!player) {
return;
this.showSkipButton({ animate: true });
}
}

currentPlayer = player;
onPlayerTimeUpdate() {
if (this.currentSegment) {
const time = this.playbackManager.currentTime(this.player) * TICKS_PER_MILLISECOND;

Events.on(player, 'promptskip', onPromptSkip);
Events.on(document, EventType.SHOW_VIDEO_OSD, onOsdChanged);
Events.on(player, 'playbackstop', onPlaybackStopped);
Events.on(player, 'timeupdate', onTimeUpdate);
if (!isInSegment(this.currentSegment, time)) {
this.currentSegment = null;
this.hideSkipButton();
}
}
}

onPlaybackStop() {
this.currentSegment = null;
this.hideSkipButton();
Events.off(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged);
}
}

Events.on(playbackManager, 'playerchange', (_e, newPlayer) => {
bindToPlayer(newPlayer);
});
bindToPlayer(playbackManager.getCurrentPlayer());
export const bindSkipSegment = (playbackManager: PlaybackManager) => new SkipSegment(playbackManager);
1 change: 0 additions & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ async function loadPlugins() {

function loadPlatformFeatures() {
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
import('./components/playback/skipsegment');
import('./components/nowPlayingBar/nowPlayingBar');
}

Expand Down

0 comments on commit 7267b66

Please sign in to comment.