From 6eb78af96505ea006e136f9a5bd22f0ba274394c Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:04:44 +0100 Subject: [PATCH] feat: show end time close #249 --- lib/i18n/de.i18n.json | 1 + lib/i18n/en.i18n.json | 1 + lib/i18n/fr.i18n.json | 1 + lib/i18n/it.i18n.json | 1 + lib/i18n/ko.i18n.json | 1 + lib/i18n/nl.i18n.json | 1 + lib/i18n/strings.g.dart | 4 +-- lib/i18n/strings_de.g.dart | 4 ++- lib/i18n/strings_en.g.dart | 6 ++++- lib/i18n/strings_fr.g.dart | 4 ++- lib/i18n/strings_it.g.dart | 4 ++- lib/i18n/strings_ko.g.dart | 4 ++- lib/i18n/strings_nl.g.dart | 4 ++- lib/i18n/strings_sv.g.dart | 4 ++- lib/i18n/strings_zh.g.dart | 4 ++- lib/i18n/sv.i18n.json | 1 + lib/i18n/zh.i18n.json | 1 + lib/utils/formatters.dart | 8 ++++++ .../desktop_video_controls.dart | 25 +++++++++++++++++++ .../video_controls/mobile_video_controls.dart | 1 + .../widgets/video_timeline_bar.dart | 14 ++++++++++- 21 files changed, 83 insertions(+), 11 deletions(-) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index db39d45c..977d76f4 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "Dreh­sperre", "timelineSlider": "Video-Zeitleiste", "volumeSlider": "Lautstärkepegel", + "endsAt": "Endet um ${time}", "backButton": "Zurück", "pipFailed": "Bild-in-Bild konnte nicht gestartet werden", "pipErrors": { diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index e93c3ec2..1d84fccb 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -300,6 +300,7 @@ "rotationLockButton": "Rotation lock", "timelineSlider": "Video timeline", "volumeSlider": "Volume level", + "endsAt": "Ends at ${time}", "backButton": "Back", "pipFailed": "Picture-in-picture failed to start", "pipErrors": { diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index d1012a3d..28d14dff 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -300,6 +300,7 @@ "rotationLockButton": "Verrouillage de rotation", "timelineSlider": "Timeline vidéo", "volumeSlider": "Niveau sonore", + "endsAt": "Fin à ${time}", "backButton": "Retour", "pipFailed": "Échec du démarrage du mode image dans l'image", "pipErrors": { diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index e2c58b2c..11173aea 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "Blocco rotazione", "timelineSlider": "Timeline video", "volumeSlider": "Livello volume", + "endsAt": "Finisce alle ${time}", "backButton": "Indietro", "pipFailed": "Impossibile avviare la modalità Picture-in-Picture", "pipErrors": { diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 17d14295..4b0080fa 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "회전 잠금", "timelineSlider": "타임라인", "volumeSlider": "볼륨 조절", + "endsAt": "${time}에 종료", "backButton": "뒤로 가기", "pipFailed": "화면 속 화면 모드를 시작할 수 없습니다", "pipErrors": { diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index d6effabc..6fe55e91 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "Rotatievergrendeling", "timelineSlider": "Videotijdlijn", "volumeSlider": "Volumeniveau", + "endsAt": "Eindigt om ${time}", "backButton": "Terug", "pipFailed": "Beeld-in-beeld kon niet worden gestart", "pipErrors": { diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 69cb285b..10880c80 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 8 -/// Strings: 4540 (567 per locale) +/// Strings: 4548 (568 per locale) /// -/// Built on 2026-01-29 at 06:02 UTC +/// Built on 2026-02-01 at 12:48 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index d92aa857..98aafdcf 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsDe implements TranslationsVideoControlsEn { @override String get rotationLockButton => 'Dreh­sperre'; @override String get timelineSlider => 'Video-Zeitleiste'; @override String get volumeSlider => 'Lautstärkepegel'; + @override String endsAt({required Object time}) => 'Endet um ${time}'; @override String get backButton => 'Zurück'; @override String get pipFailed => 'Bild-in-Bild konnte nicht gestartet werden'; @override late final _TranslationsVideoControlsPipErrorsDe pipErrors = _TranslationsVideoControlsPipErrorsDe._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsDe { 'videoControls.rotationLockButton' => 'Dreh­sperre', 'videoControls.timelineSlider' => 'Video-Zeitleiste', 'videoControls.volumeSlider' => 'Lautstärkepegel', + 'videoControls.endsAt' => ({required Object time}) => 'Endet um ${time}', 'videoControls.backButton' => 'Zurück', 'videoControls.pipFailed' => 'Bild-in-Bild konnte nicht gestartet werden', 'videoControls.pipErrors.androidVersion' => 'Erfordert Android 8.0 oder neuer', @@ -1496,9 +1498,9 @@ extension on TranslationsDe { 'collections.addedToCollection' => 'Zur Sammlung hinzugefügt', 'collections.errorAddingToCollection' => 'Fehler beim Hinzufügen zur Sammlung', 'collections.created' => 'Sammlung erstellt', - 'collections.removeFromCollection' => 'Aus Sammlung entfernen', _ => null, } ?? switch (path) { + 'collections.removeFromCollection' => 'Aus Sammlung entfernen', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '"${title}" aus dieser Sammlung entfernen?', 'collections.removedFromCollection' => 'Aus Sammlung entfernt', 'collections.removeFromCollectionFailed' => 'Entfernen aus Sammlung fehlgeschlagen', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index e3af9f11..a892c0c3 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -964,6 +964,9 @@ class TranslationsVideoControlsEn { /// en: 'Volume level' String get volumeSlider => 'Volume level'; + /// en: 'Ends at ${time}' + String endsAt({required Object time}) => 'Ends at ${time}'; + /// en: 'Back' String get backButton => 'Back'; @@ -2393,6 +2396,7 @@ extension on Translations { 'videoControls.rotationLockButton' => 'Rotation lock', 'videoControls.timelineSlider' => 'Video timeline', 'videoControls.volumeSlider' => 'Volume level', + 'videoControls.endsAt' => ({required Object time}) => 'Ends at ${time}', 'videoControls.backButton' => 'Back', 'videoControls.pipFailed' => 'Picture-in-picture failed to start', 'videoControls.pipErrors.androidVersion' => 'Requires Android 8.0 or newer', @@ -2632,9 +2636,9 @@ extension on Translations { 'watchTogether.hostOnly' => 'Host Only', 'watchTogether.anyone' => 'Anyone', 'watchTogether.hostingSession' => 'Hosting Session', - 'watchTogether.inSession' => 'In Session', _ => null, } ?? switch (path) { + 'watchTogether.inSession' => 'In Session', 'watchTogether.sessionCode' => 'Session Code', 'watchTogether.hostControlsPlayback' => 'Host controls playback', 'watchTogether.anyoneCanControl' => 'Anyone can control playback', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index b9657ff5..68939f66 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -451,6 +451,7 @@ class _TranslationsVideoControlsFr implements TranslationsVideoControlsEn { @override String get rotationLockButton => 'Verrouillage de rotation'; @override String get timelineSlider => 'Timeline vidéo'; @override String get volumeSlider => 'Niveau sonore'; + @override String endsAt({required Object time}) => 'Fin à ${time}'; @override String get backButton => 'Retour'; @override String get pipFailed => 'Échec du démarrage du mode image dans l\'image'; @override late final _TranslationsVideoControlsPipErrorsFr pipErrors = _TranslationsVideoControlsPipErrorsFr._(_root); @@ -1255,6 +1256,7 @@ extension on TranslationsFr { 'videoControls.rotationLockButton' => 'Verrouillage de rotation', 'videoControls.timelineSlider' => 'Timeline vidéo', 'videoControls.volumeSlider' => 'Niveau sonore', + 'videoControls.endsAt' => ({required Object time}) => 'Fin à ${time}', 'videoControls.backButton' => 'Retour', 'videoControls.pipFailed' => 'Échec du démarrage du mode image dans l\'image', 'videoControls.pipErrors.androidVersion' => 'Nécessite Android 8.0 ou plus récent', @@ -1494,9 +1496,9 @@ extension on TranslationsFr { 'watchTogether.hostOnly' => 'Hôte uniquement', 'watchTogether.anyone' => 'N\'importe qui', 'watchTogether.hostingSession' => 'Session d\'hébergement', - 'watchTogether.inSession' => 'En session', _ => null, } ?? switch (path) { + 'watchTogether.inSession' => 'En session', 'watchTogether.sessionCode' => 'Code de session', 'watchTogether.hostControlsPlayback' => 'L\'hôte contrôle la lecture', 'watchTogether.anyoneCanControl' => 'Tout le monde peut contrôler la lecture', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 56b569c2..b0b91174 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsIt implements TranslationsVideoControlsEn { @override String get rotationLockButton => 'Blocco rotazione'; @override String get timelineSlider => 'Timeline video'; @override String get volumeSlider => 'Livello volume'; + @override String endsAt({required Object time}) => 'Finisce alle ${time}'; @override String get backButton => 'Indietro'; @override String get pipFailed => 'Impossibile avviare la modalità Picture-in-Picture'; @override late final _TranslationsVideoControlsPipErrorsIt pipErrors = _TranslationsVideoControlsPipErrorsIt._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsIt { 'videoControls.rotationLockButton' => 'Blocco rotazione', 'videoControls.timelineSlider' => 'Timeline video', 'videoControls.volumeSlider' => 'Livello volume', + 'videoControls.endsAt' => ({required Object time}) => 'Finisce alle ${time}', 'videoControls.backButton' => 'Indietro', 'videoControls.pipFailed' => 'Impossibile avviare la modalità Picture-in-Picture', 'videoControls.pipErrors.androidVersion' => 'Richiede Android 8.0 o versioni successive', @@ -1496,9 +1498,9 @@ extension on TranslationsIt { 'collections.addedToCollection' => 'Aggiunto alla raccolta', 'collections.errorAddingToCollection' => 'Errore nell\'aggiunta alla raccolta', 'collections.created' => 'Raccolta creata', - 'collections.removeFromCollection' => 'Rimuovi dalla raccolta', _ => null, } ?? switch (path) { + 'collections.removeFromCollection' => 'Rimuovi dalla raccolta', 'collections.removeFromCollectionConfirm' => ({required Object title}) => 'Rimuovere "${title}" da questa raccolta?', 'collections.removedFromCollection' => 'Rimosso dalla raccolta', 'collections.removeFromCollectionFailed' => 'Impossibile rimuovere dalla raccolta', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 5374d8d0..826d6373 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsKo implements TranslationsVideoControlsEn { @override String get rotationLockButton => '회전 잠금'; @override String get timelineSlider => '타임라인'; @override String get volumeSlider => '볼륨 조절'; + @override String endsAt({required Object time}) => '${time}에 종료'; @override String get backButton => '뒤로 가기'; @override String get pipFailed => '화면 속 화면 모드를 시작할 수 없습니다'; @override late final _TranslationsVideoControlsPipErrorsKo pipErrors = _TranslationsVideoControlsPipErrorsKo._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsKo { 'videoControls.rotationLockButton' => '회전 잠금', 'videoControls.timelineSlider' => '타임라인', 'videoControls.volumeSlider' => '볼륨 조절', + 'videoControls.endsAt' => ({required Object time}) => '${time}에 종료', 'videoControls.backButton' => '뒤로 가기', 'videoControls.pipFailed' => '화면 속 화면 모드를 시작할 수 없습니다', 'videoControls.pipErrors.androidVersion' => 'Android 8.0 이상이 필요합니다', @@ -1496,9 +1498,9 @@ extension on TranslationsKo { 'watchTogether.controlMode' => '제어 모드', 'watchTogether.controlModeQuestion' => '누가 재생을 제어할 수 있나요?', 'watchTogether.hostOnly' => '호스트만', - 'watchTogether.anyone' => '누구나', _ => null, } ?? switch (path) { + 'watchTogether.anyone' => '누구나', 'watchTogether.hostingSession' => '세션 호스팅', 'watchTogether.inSession' => '세션 중', 'watchTogether.sessionCode' => '세션 코드', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index c5dd9d8e..6b6d8530 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsNl implements TranslationsVideoControlsEn { @override String get rotationLockButton => 'Rotatievergrendeling'; @override String get timelineSlider => 'Videotijdlijn'; @override String get volumeSlider => 'Volumeniveau'; + @override String endsAt({required Object time}) => 'Eindigt om ${time}'; @override String get backButton => 'Terug'; @override String get pipFailed => 'Beeld-in-beeld kon niet worden gestart'; @override late final _TranslationsVideoControlsPipErrorsNl pipErrors = _TranslationsVideoControlsPipErrorsNl._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsNl { 'videoControls.rotationLockButton' => 'Rotatievergrendeling', 'videoControls.timelineSlider' => 'Videotijdlijn', 'videoControls.volumeSlider' => 'Volumeniveau', + 'videoControls.endsAt' => ({required Object time}) => 'Eindigt om ${time}', 'videoControls.backButton' => 'Terug', 'videoControls.pipFailed' => 'Beeld-in-beeld kon niet worden gestart', 'videoControls.pipErrors.androidVersion' => 'Vereist Android 8.0 of nieuwer', @@ -1496,9 +1498,9 @@ extension on TranslationsNl { 'collections.addedToCollection' => 'Toegevoegd aan collectie', 'collections.errorAddingToCollection' => 'Fout bij toevoegen aan collectie', 'collections.created' => 'Collectie gemaakt', - 'collections.removeFromCollection' => 'Verwijderen uit collectie', _ => null, } ?? switch (path) { + 'collections.removeFromCollection' => 'Verwijderen uit collectie', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '"${title}" uit deze collectie verwijderen?', 'collections.removedFromCollection' => 'Uit collectie verwijderd', 'collections.removeFromCollectionFailed' => 'Verwijderen uit collectie mislukt', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 18a999f9..878ccaca 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsSv implements TranslationsVideoControlsEn { @override String get rotationLockButton => 'Rotationslås'; @override String get timelineSlider => 'Videotidslinje'; @override String get volumeSlider => 'Volymnivå'; + @override String endsAt({required Object time}) => 'Slutar ${time}'; @override String get backButton => 'Tillbaka'; @override String get pipFailed => 'Bild-i-bild kunde inte starta'; @override late final _TranslationsVideoControlsPipErrorsSv pipErrors = _TranslationsVideoControlsPipErrorsSv._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsSv { 'videoControls.rotationLockButton' => 'Rotationslås', 'videoControls.timelineSlider' => 'Videotidslinje', 'videoControls.volumeSlider' => 'Volymnivå', + 'videoControls.endsAt' => ({required Object time}) => 'Slutar ${time}', 'videoControls.backButton' => 'Tillbaka', 'videoControls.pipFailed' => 'Bild-i-bild kunde inte starta', 'videoControls.pipErrors.androidVersion' => 'Kräver Android 8.0 eller nyare', @@ -1496,9 +1498,9 @@ extension on TranslationsSv { 'collections.addedToCollection' => 'Tillagd i samling', 'collections.errorAddingToCollection' => 'Fel vid tillägg i samling', 'collections.created' => 'Samling skapad', - 'collections.removeFromCollection' => 'Ta bort från samling', _ => null, } ?? switch (path) { + 'collections.removeFromCollection' => 'Ta bort från samling', 'collections.removeFromCollectionConfirm' => ({required Object title}) => 'Ta bort "${title}" från denna samling?', 'collections.removedFromCollection' => 'Borttagen från samling', 'collections.removeFromCollectionFailed' => 'Misslyckades med att ta bort från samling', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 0b6c2e47..7cc08eaf 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -452,6 +452,7 @@ class _TranslationsVideoControlsZh implements TranslationsVideoControlsEn { @override String get rotationLockButton => '旋转锁定'; @override String get timelineSlider => '视频时间轴'; @override String get volumeSlider => '音量调节'; + @override String endsAt({required Object time}) => '${time} 结束'; @override String get backButton => '返回'; @override String get pipFailed => '画中画启动失败'; @override late final _TranslationsVideoControlsPipErrorsZh pipErrors = _TranslationsVideoControlsPipErrorsZh._(_root); @@ -1258,6 +1259,7 @@ extension on TranslationsZh { 'videoControls.rotationLockButton' => '旋转锁定', 'videoControls.timelineSlider' => '视频时间轴', 'videoControls.volumeSlider' => '音量调节', + 'videoControls.endsAt' => ({required Object time}) => '${time} 结束', 'videoControls.backButton' => '返回', 'videoControls.pipFailed' => '画中画启动失败', 'videoControls.pipErrors.androidVersion' => '需要 Android 8.0 或更高版本', @@ -1496,9 +1498,9 @@ extension on TranslationsZh { 'collections.addedToCollection' => '已添加到合集', 'collections.errorAddingToCollection' => '添加到合集失败', 'collections.created' => '已创建合集', - 'collections.removeFromCollection' => '从合集移除', _ => null, } ?? switch (path) { + 'collections.removeFromCollection' => '从合集移除', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '将“${title}”从此合集移除?', 'collections.removedFromCollection' => '已从合集移除', 'collections.removeFromCollectionFailed' => '从合集移除失败', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 1eca6aa9..02e01794 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "Rotationslås", "timelineSlider": "Videotidslinje", "volumeSlider": "Volymnivå", + "endsAt": "Slutar ${time}", "backButton": "Tillbaka", "pipFailed": "Bild-i-bild kunde inte starta", "pipErrors": { diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 9f2ff71a..fcb46b24 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -301,6 +301,7 @@ "rotationLockButton": "旋转锁定", "timelineSlider": "视频时间轴", "volumeSlider": "音量调节", + "endsAt": "${time} 结束", "backButton": "返回", "pipFailed": "画中画启动失败", "pipErrors": { diff --git a/lib/utils/formatters.dart b/lib/utils/formatters.dart index a21de477..d3baae62 100644 --- a/lib/utils/formatters.dart +++ b/lib/utils/formatters.dart @@ -180,6 +180,14 @@ DurationLocale _getDurationLocale() { } } +/// Formats the clock time at which media will finish playing, given the remaining duration. +/// Returns a localized time string like "6:12 PM" or "18:12" depending on locale. +String formatFinishTime(Duration remaining) { + final finishTime = DateTime.now().add(remaining); + final formatter = DateFormat.jm(LocaleSettings.currentLocale.languageCode); + return formatter.format(finishTime); +} + /// Takes a list of strings and returns one long string with each item in the list concatenated by a bullet String toBulletedString(List parts) { return parts.join(' · '); diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index bca02c49..0ed686cf 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -12,6 +12,7 @@ import '../../models/plex_media_version.dart'; import '../../models/plex_metadata.dart'; import '../../services/fullscreen_state_manager.dart'; import '../../utils/desktop_window_padding.dart'; +import '../../utils/formatters.dart'; import '../../i18n/strings.g.dart'; import '../../focus/focusable_wrapper.dart'; import '../../services/shader_service.dart'; @@ -506,6 +507,30 @@ class DesktopVideoControlsState extends State { semanticLabel: t.videoControls.nextButton, ), ), + // Finish time + StreamBuilder( + stream: widget.player.streams.position, + initialData: widget.player.state.position, + builder: (context, posSnap) { + return StreamBuilder( + stream: widget.player.streams.duration, + initialData: widget.player.state.duration, + builder: (context, durSnap) { + final position = posSnap.data ?? Duration.zero; + final duration = durSnap.data ?? Duration.zero; + final remaining = duration - position; + if (remaining.inSeconds <= 0) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + t.videoControls.endsAt(time: formatFinishTime(remaining)), + style: const TextStyle(color: Colors.white70, fontSize: 13), + ), + ); + }, + ); + }, + ), const Spacer(), // Volume control VolumeControl( diff --git a/lib/widgets/video_controls/mobile_video_controls.dart b/lib/widgets/video_controls/mobile_video_controls.dart index 71a3331a..8d4b8f4a 100644 --- a/lib/widgets/video_controls/mobile_video_controls.dart +++ b/lib/widgets/video_controls/mobile_video_controls.dart @@ -166,6 +166,7 @@ class MobileVideoControls extends StatelessWidget { onSeekEnd: onSeekEnd, horizontalLayout: false, enabled: canControl, + showFinishTime: true, ), ), ); diff --git a/lib/widgets/video_controls/widgets/video_timeline_bar.dart b/lib/widgets/video_controls/widgets/video_timeline_bar.dart index 197f0a70..28066c56 100644 --- a/lib/widgets/video_controls/widgets/video_timeline_bar.dart +++ b/lib/widgets/video_controls/widgets/video_timeline_bar.dart @@ -33,6 +33,9 @@ class VideoTimelineBar extends StatelessWidget { /// Whether the timeline is enabled for interaction. final bool enabled; + /// Whether to show the estimated finish time next to the remaining timestamp (mobile). + final bool showFinishTime; + const VideoTimelineBar({ super.key, required this.player, @@ -45,6 +48,7 @@ class VideoTimelineBar extends StatelessWidget { this.onKeyEvent, this.onFocusChange, this.enabled = true, + this.showFinishTime = false, }); @override @@ -92,7 +96,7 @@ class VideoTimelineBar extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [_buildTimestamp(position), _buildTimestamp(remaining)], + children: [_buildTimestamp(position), _buildRemainingTimestamp(remaining)], ), ), ], @@ -103,6 +107,14 @@ class VideoTimelineBar extends StatelessWidget { return Text(formatDurationTimestamp(time), style: const TextStyle(color: Colors.white, fontSize: 14)); } + Widget _buildRemainingTimestamp(Duration remaining) { + if (!showFinishTime || remaining.inSeconds >= 0) { + return _buildTimestamp(remaining); + } + final text = '${formatDurationTimestamp(remaining)} · ${formatFinishTime(remaining.abs())}'; + return Text(text, style: const TextStyle(color: Colors.white, fontSize: 14)); + } + Widget _buildSlider(Duration position, Duration duration) { return TimelineSlider( position: position,