1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Add 'prompt to skip' to media segments

This commit is contained in:
viown 2024-10-12 15:43:40 +03:00
parent effa74356b
commit 0fb38c6894
7 changed files with 216 additions and 1 deletions

View file

@ -3,5 +3,6 @@
*/ */
export enum MediaSegmentAction { export enum MediaSegmentAction {
None = 'None', None = 'None',
PromptToSkip = 'PromptToSkip',
Skip = 'Skip' Skip = 'Skip'
} }

View file

@ -66,6 +66,8 @@ class MediaSegmentManager extends PlaybackSubscriber {
console.debug('[MediaSegmentManager] skipping to next item in queue'); console.debug('[MediaSegmentManager] skipping to next item in queue');
this.playbackManager.nextTrack(this.player); this.playbackManager.nextTrack(this.player);
} }
} else if (action === MediaSegmentAction.PromptToSkip) {
this.playbackManager.promptToSkip(mediaSegment);
} }
} }

View file

@ -13,7 +13,7 @@ const isBeforeSegment = (segment: MediaSegmentDto, time: number, direction: numb
); );
}; };
const isInSegment = (segment: MediaSegmentDto, time: number) => ( export const isInSegment = (segment: MediaSegmentDto, time: number) => (
typeof segment.StartTicks !== 'undefined' typeof segment.StartTicks !== 'undefined'
&& segment.StartTicks <= time && segment.StartTicks <= time
&& (typeof segment.EndTicks === 'undefined' || segment.EndTicks > time) && (typeof segment.EndTicks === 'undefined' || segment.EndTicks > time)

View file

@ -26,6 +26,8 @@ import { MediaError } from 'types/mediaError';
import { getMediaError } from 'utils/mediaError'; import { getMediaError } from 'utils/mediaError';
import { toApi } from 'utils/jellyfin-apiclient/compat'; import { toApi } from 'utils/jellyfin-apiclient/compat';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js'; 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; const UNLIMITED_ITEMS = -1;
@ -933,6 +935,12 @@ export class PlaybackManager {
return Promise.resolve(self._playQueueManager.getPlaylist()); return Promise.resolve(self._playQueueManager.getPlaylist());
}; };
self.promptToSkip = function (mediaSegment) {
if (mediaSegment && this._skipSegment) {
this._skipSegment.onPromptSkip(mediaSegment);
}
};
function removeCurrentPlayer(player) { function removeCurrentPlayer(player) {
const previousPlayer = self._currentPlayer; const previousPlayer = self._currentPlayer;
@ -3666,6 +3674,9 @@ export class PlaybackManager {
} }
bindMediaSegmentManager(self); bindMediaSegmentManager(self);
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
this._skipSegment = bindSkipSegment(self);
}
} }
getCurrentPlayer() { getCurrentPlayer() {
@ -3680,6 +3691,10 @@ export class PlaybackManager {
return this.getCurrentTicks(player) / 10000; return this.getCurrentTicks(player) / 10000;
} }
getNextItem() {
return this._playQueueManager.getNextItemInfo();
}
nextItem(player = this._currentPlayer) { nextItem(player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) { if (player && !enableLocalPlaylistManagement(player)) {
return player.nextItem(); return player.nextItem();

View file

@ -0,0 +1,32 @@
.skip-button {
display: flex;
align-items: center;
position: fixed;
bottom: 18%;
right: 16%;
z-index: 10000;
padding: 12px 20px;
color: black;
border: none;
border-radius: 100px;
font-weight: bold;
font-size: 1.2em;
transition: opacity 200ms ease-out;
gap: 3px;
box-shadow: 7px 6px 15px -14px rgba(0, 0, 0, 0.65);
cursor: pointer;
}
@media (orientation: landscape) and (max-height: 500px) {
.skip-button {
bottom: 27%;
}
}
.no-transition {
transition: none;
}
.skip-button-hidden {
opacity: 0;
}

View file

@ -0,0 +1,162 @@
import { PlaybackManager } from './playbackmanager';
import { TICKS_PER_MILLISECOND } from 'constants/time';
import { MediaSegmentDto, MediaSegmentType } from '@jellyfin/sdk/lib/generated-client';
import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber';
import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments';
import Events, { type Event } from '../../utils/events';
import { EventType } from 'types/eventType';
import './skipbutton.scss';
import dom from 'scripts/dom';
import globalize from 'lib/globalize';
interface ShowOptions {
animate?: boolean;
keep?: boolean;
}
class SkipSegment extends PlaybackSubscriber {
private skipElement: HTMLButtonElement | undefined;
private currentSegment: MediaSegmentDto | null | undefined;
private hideTimeout: ReturnType<typeof setTimeout> | null | undefined;
constructor(playbackManager: PlaybackManager) {
super(playbackManager);
this.onOsdChanged = this.onOsdChanged.bind(this);
Events.on(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged);
}
onOsdChanged(_e: Event, isOpen: boolean) {
if (this.currentSegment) {
if (isOpen) {
this.showSkipButton({
animate: false,
keep: true
});
} else if (!this.hideTimeout) {
this.hideSkipButton();
}
}
}
onHideComplete() {
if (this.skipElement) {
this.skipElement.classList.add('hide');
}
}
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');
elem.addEventListener('click', () => {
if (this.currentSegment) {
this.playbackManager.seek(this.currentSegment.EndTicks);
}
});
document.body.appendChild(elem);
this.skipElement = elem;
}
}
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>';
}
}
showSkipButton(options: ShowOptions) {
const elem = this.skipElement;
if (elem) {
this.clearHideTimeout();
dom.removeEventListener(elem, dom.whichTransitionEvent(), this.onHideComplete, {
once: true
});
elem.classList.remove('hide');
if (!options.animate) {
elem.classList.add('no-transition');
} else {
elem.classList.remove('no-transition');
}
void elem.offsetWidth;
requestAnimationFrame(() => {
elem.classList.remove('skip-button-hidden');
if (!options.keep) {
this.hideTimeout = setTimeout(this.hideSkipButton.bind(this), 6000);
}
});
}
}
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
});
});
}
}
clearHideTimeout() {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
}
onPromptSkip(segment: MediaSegmentDto) {
if (!this.currentSegment) {
this.currentSegment = segment;
this.createSkipElement();
this.setButtonText();
this.showSkipButton({ animate: true });
}
}
onPlayerTimeUpdate() {
if (this.currentSegment) {
const time = this.playbackManager.currentTime(this.player) * TICKS_PER_MILLISECOND;
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);
}
}
export const bindSkipSegment = (playbackManager: PlaybackManager) => new SkipSegment(playbackManager);

View file

@ -1072,7 +1072,10 @@
"MediaInfoVideoRange": "Video range", "MediaInfoVideoRange": "Video range",
"MediaIsBeingConverted": "The media is being converted into a format that is compatible with the device that is playing the media.", "MediaIsBeingConverted": "The media is being converted into a format that is compatible with the device that is playing the media.",
"MediaSegmentAction.None": "None", "MediaSegmentAction.None": "None",
"MediaSegmentAction.PromptToSkip": "Prompt To Skip",
"MediaSegmentAction.Skip": "Skip", "MediaSegmentAction.Skip": "Skip",
"MediaSegmentNextEpisode": "Next Episode",
"MediaSegmentSkipPrompt": "Skip {0}",
"MediaSegmentType.Commercial": "Commercial", "MediaSegmentType.Commercial": "Commercial",
"MediaSegmentType.Intro": "Intro", "MediaSegmentType.Intro": "Intro",
"MediaSegmentType.Outro": "Outro", "MediaSegmentType.Outro": "Outro",