diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index f7eb8b5d..f0cf677c 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -57,6 +57,8 @@ "backToHome": "Back to home", "backToHomeShort": "Back", "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "{{timeLeft}} left", + "finishAt": "Finish at {{timeFinished}}", "buttons": { "episodes": "Episodes", "source": "Source", diff --git a/src/setup/locales/fr/translation.json b/src/setup/locales/fr/translation.json index e5c669ce..fe9d73eb 100644 --- a/src/setup/locales/fr/translation.json +++ b/src/setup/locales/fr/translation.json @@ -39,13 +39,16 @@ "backToHome": "Retour à la page d'accueil", "backToHomeShort": "Retour", "seasonAndEpisode": "S{{season}} E{{episode}}", + "timeLeft": "{{timeLeft}} restant", + "finishAt": "Terminer à {{timeFinished}}", "buttons": { "episodes": "Épisodes", "source": "Source", "captions": "Sous-titres", "download": "Télécharger", "settings": "Paramètres", - "pictureInPicture": "Image dans l'image" + "pictureInPicture": "Image dans l'image", + "playbackSpeed": "Vitesse" }, "popouts": { "sources": "Sources", diff --git a/src/video/components/actions/TimeAction.tsx b/src/video/components/actions/TimeAction.tsx index 493f6d26..bab9e101 100644 --- a/src/video/components/actions/TimeAction.tsx +++ b/src/video/components/actions/TimeAction.tsx @@ -1,6 +1,11 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; +import { useTranslation } from "react-i18next"; import { useMediaPlaying } from "@/video/state/logic/mediaplaying"; import { useProgress } from "@/video/state/logic/progress"; +import { useInterface } from "@/video/state/logic/interface"; +import { VideoPlayerTimeFormat } from "@/video/state/types"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { useControls } from "@/video/state/logic/controls"; function durationExceedsHour(secs: number): boolean { return secs > 60 * 60; @@ -37,19 +42,71 @@ export function TimeAction(props: Props) { const descriptor = useVideoPlayerDescriptor(); const videoTime = useProgress(descriptor); const mediaPlaying = useMediaPlaying(descriptor); + const { setTimeFormat } = useControls(descriptor); + const { timeFormat } = useInterface(descriptor); + const { isMobile } = useIsMobile(); + const { t } = useTranslation(); const hasHours = durationExceedsHour(videoTime.duration); - const time = formatSeconds( + + const currentTime = formatSeconds( mediaPlaying.isDragSeeking ? videoTime.draggingTime : videoTime.time, hasHours ); const duration = formatSeconds(videoTime.duration, hasHours); + const timeLeft = formatSeconds( + (videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed, + hasHours + ); + const timeFinished = new Date( + new Date().getTime() + + (videoTime.duration * 1000) / mediaPlaying.playbackSpeed + ).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + hour12: true, + }); + const formattedTimeFinished = ` - ${t("videoPlayer.finishAt", { + timeFinished, + })}`; + + let formattedTime: string; + + if (timeFormat === VideoPlayerTimeFormat.REGULAR) { + formattedTime = `${currentTime} ${props.noDuration ? "" : `/ ${duration}`}`; + } else if (timeFormat === VideoPlayerTimeFormat.REMAINING && !isMobile) { + formattedTime = `${t("videoPlayer.timeLeft", { + timeLeft, + })}${videoTime.time === videoTime.duration ? "" : formattedTimeFinished} `; + } else if (timeFormat === VideoPlayerTimeFormat.REMAINING && isMobile) { + formattedTime = `-${timeLeft}`; + } else { + formattedTime = ""; + } return ( -
-

- {time} {props.noDuration ? "" : `/ ${duration}`} -

-
+ ); } diff --git a/src/video/state/init.ts b/src/video/state/init.ts index bd4037fe..6c60ad54 100644 --- a/src/video/state/init.ts +++ b/src/video/state/init.ts @@ -32,6 +32,7 @@ function initPlayer(): VideoPlayerState { isFocused: false, leftControlHovering: false, popoutBounds: null, + timeFormat: 0, }, mediaPlaying: { diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index e6d33369..ccd83add 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -1,7 +1,7 @@ import { updateInterface } from "@/video/state/logic/interface"; import { updateMeta } from "@/video/state/logic/meta"; import { updateProgress } from "@/video/state/logic/progress"; -import { VideoPlayerMeta } from "@/video/state/types"; +import { VideoPlayerMeta, VideoPlayerTimeFormat } from "@/video/state/types"; import { getPlayerState } from "../cache"; import { VideoPlayerStateController } from "../providers/providerTypes"; @@ -15,6 +15,7 @@ export type ControlMethods = { setDraggingTime(num: number): void; togglePictureInPicture(): void; setPlaybackSpeed(num: number): void; + setTimeFormat(num: VideoPlayerTimeFormat): void; }; export function useControls( @@ -110,5 +111,9 @@ export function useControls( state.stateProvider?.setPlaybackSpeed(num); updateInterface(descriptor, state); }, + setTimeFormat(format) { + state.interface.timeFormat = format; + updateInterface(descriptor, state); + }, }; } diff --git a/src/video/state/logic/interface.ts b/src/video/state/logic/interface.ts index 2f22823f..4ac47a3a 100644 --- a/src/video/state/logic/interface.ts +++ b/src/video/state/logic/interface.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { getPlayerState } from "../cache"; import { listenEvent, sendEvent, unlistenEvent } from "../events"; -import { VideoPlayerState } from "../types"; +import { VideoPlayerState, VideoPlayerTimeFormat } from "../types"; export type VideoInterfaceEvent = { popout: string | null; @@ -9,6 +9,7 @@ export type VideoInterfaceEvent = { isFocused: boolean; isFullscreen: boolean; popoutBounds: null | DOMRect; + timeFormat: VideoPlayerTimeFormat; }; function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { @@ -18,6 +19,7 @@ function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { isFocused: state.interface.isFocused, isFullscreen: state.interface.isFullscreen, popoutBounds: state.interface.popoutBounds, + timeFormat: state.interface.timeFormat, }; } diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 1ba9ef7a..e5e403da 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -22,6 +22,11 @@ export type VideoPlayerMeta = { }[]; }; +export enum VideoPlayerTimeFormat { + REGULAR = 0, + REMAINING = 1, +} + export type VideoPlayerState = { // state related to the user interface interface: { @@ -30,6 +35,7 @@ export type VideoPlayerState = { isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused) leftControlHovering: boolean; // is the cursor hovered over the left side of player controls popoutBounds: null | DOMRect; // bounding box of current popout + timeFormat: VideoPlayerTimeFormat; // Time format of the video player }; // state related to the playing state of the media