diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index aae488e5..85d845e5 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 6 -/// Strings: 2608 (434 per locale) +/// Strings: 2614 (435 per locale) /// -/// Built on 2025-12-12 at 16:59 UTC +/// Built on 2025-12-19 at 00:18 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -551,6 +551,7 @@ class _StringsVideoControlsEn { String get aspectRatioButton => 'Aspect ratio'; String get fullscreenButton => 'Enter fullscreen'; String get exitFullscreenButton => 'Exit fullscreen'; + String get alwaysOnTopButton => 'Always on top'; String get rotationLockButton => 'Rotation lock'; String get timelineSlider => 'Video timeline'; String get volumeSlider => 'Volume level'; @@ -1609,6 +1610,8 @@ class _StringsVideoControlsDe implements _StringsVideoControlsEn { @override String get exitFullscreenButton => 'Vollbild verlassen'; @override + String get alwaysOnTopButton => 'Immer im Vordergrund'; + @override String get rotationLockButton => 'Dreh­sperre'; @override String get timelineSlider => 'Video-Zeitleiste'; @@ -2906,6 +2909,8 @@ class _StringsVideoControlsIt implements _StringsVideoControlsEn { @override String get exitFullscreenButton => 'Esci da schermo intero'; @override + String get alwaysOnTopButton => 'Sempre in primo piano'; + @override String get rotationLockButton => 'Blocco rotazione'; @override String get timelineSlider => 'Timeline video'; @@ -4201,6 +4206,8 @@ class _StringsVideoControlsNl implements _StringsVideoControlsEn { @override String get exitFullscreenButton => 'Volledig scherm verlaten'; @override + String get alwaysOnTopButton => 'Altijd bovenop'; + @override String get rotationLockButton => 'Rotatievergrendeling'; @override String get timelineSlider => 'Videotijdlijn'; @@ -5496,6 +5503,8 @@ class _StringsVideoControlsSv implements _StringsVideoControlsEn { @override String get exitFullscreenButton => 'Avsluta helskärm'; @override + String get alwaysOnTopButton => 'Alltid överst'; + @override String get rotationLockButton => 'Rotationslås'; @override String get timelineSlider => 'Videotidslinje'; @@ -6315,7 +6324,7 @@ class _StringsScreensZh implements _StringsScreensEn { @override String get selectServer => '选择服务器'; @override - String get switchProfile => '切换配置文件'; + String get switchProfile => '切换用户'; @override String get subtitleStyling => '字幕样式'; @override @@ -6788,6 +6797,8 @@ class _StringsVideoControlsZh implements _StringsVideoControlsEn { @override String get exitFullscreenButton => '退出全屏'; @override + String get alwaysOnTopButton => '置顶窗口'; + @override String get rotationLockButton => '旋转锁定'; @override String get timelineSlider => '视频时间轴'; @@ -6940,7 +6951,7 @@ class _StringsDiscoverZh implements _StringsDiscoverEn { @override String get title => '发现'; @override - String get switchProfile => '切换配置文件'; + String get switchProfile => '切换用户'; @override String get switchServer => '切换服务器'; @override @@ -7850,6 +7861,8 @@ extension on Translations { return 'Enter fullscreen'; case 'videoControls.exitFullscreenButton': return 'Exit fullscreen'; + case 'videoControls.alwaysOnTopButton': + return 'Always on top'; case 'videoControls.rotationLockButton': return 'Rotation lock'; case 'videoControls.timelineSlider': @@ -8730,6 +8743,8 @@ extension on _StringsDe { return 'Vollbild aktivieren'; case 'videoControls.exitFullscreenButton': return 'Vollbild verlassen'; + case 'videoControls.alwaysOnTopButton': + return 'Immer im Vordergrund'; case 'videoControls.rotationLockButton': return 'Dreh­sperre'; case 'videoControls.timelineSlider': @@ -9614,6 +9629,8 @@ extension on _StringsIt { return 'Attiva schermo intero'; case 'videoControls.exitFullscreenButton': return 'Esci da schermo intero'; + case 'videoControls.alwaysOnTopButton': + return 'Sempre in primo piano'; case 'videoControls.rotationLockButton': return 'Blocco rotazione'; case 'videoControls.timelineSlider': @@ -10498,6 +10515,8 @@ extension on _StringsNl { return 'Volledig scherm activeren'; case 'videoControls.exitFullscreenButton': return 'Volledig scherm verlaten'; + case 'videoControls.alwaysOnTopButton': + return 'Altijd bovenop'; case 'videoControls.rotationLockButton': return 'Rotatievergrendeling'; case 'videoControls.timelineSlider': @@ -11382,6 +11401,8 @@ extension on _StringsSv { return 'Aktivera helskärm'; case 'videoControls.exitFullscreenButton': return 'Avsluta helskärm'; + case 'videoControls.alwaysOnTopButton': + return 'Alltid överst'; case 'videoControls.rotationLockButton': return 'Rotationslås'; case 'videoControls.timelineSlider': @@ -11893,7 +11914,7 @@ extension on _StringsZh { case 'screens.selectServer': return '选择服务器'; case 'screens.switchProfile': - return '切换配置文件'; + return '切换用户'; case 'screens.subtitleStyling': return '字幕样式'; case 'screens.search': @@ -12265,6 +12286,8 @@ extension on _StringsZh { return '进入全屏'; case 'videoControls.exitFullscreenButton': return '退出全屏'; + case 'videoControls.alwaysOnTopButton': + return '置顶窗口'; case 'videoControls.rotationLockButton': return '旋转锁定'; case 'videoControls.timelineSlider': @@ -12368,7 +12391,7 @@ extension on _StringsZh { case 'discover.title': return '发现'; case 'discover.switchProfile': - return '切换配置文件'; + return '切换用户'; case 'discover.switchServer': return '切换服务器'; case 'discover.logout': diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index d0459cb1..66b343f0 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -243,6 +243,7 @@ "aspectRatioButton": "Aspect ratio", "fullscreenButton": "Enter fullscreen", "exitFullscreenButton": "Exit fullscreen", + "alwaysOnTopButton": "Always on top", "rotationLockButton": "Rotation lock", "timelineSlider": "Video timeline", "volumeSlider": "Volume level", diff --git a/lib/i18n/strings_de.i18n.json b/lib/i18n/strings_de.i18n.json index 4a8ecd13..b69cc7ac 100644 --- a/lib/i18n/strings_de.i18n.json +++ b/lib/i18n/strings_de.i18n.json @@ -244,6 +244,7 @@ "aspectRatioButton": "Seitenverhältnis", "fullscreenButton": "Vollbild aktivieren", "exitFullscreenButton": "Vollbild verlassen", + "alwaysOnTopButton": "Immer im Vordergrund", "rotationLockButton": "Dreh­sperre", "timelineSlider": "Video-Zeitleiste", "volumeSlider": "Lautstärkepegel", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index cfdfabfc..f658a443 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -244,6 +244,7 @@ "aspectRatioButton": "Proporzioni", "fullscreenButton": "Attiva schermo intero", "exitFullscreenButton": "Esci da schermo intero", + "alwaysOnTopButton": "Sempre in primo piano", "rotationLockButton": "Blocco rotazione", "timelineSlider": "Timeline video", "volumeSlider": "Livello volume", diff --git a/lib/i18n/strings_nl.i18n.json b/lib/i18n/strings_nl.i18n.json index ebe5f008..f76ca1c4 100644 --- a/lib/i18n/strings_nl.i18n.json +++ b/lib/i18n/strings_nl.i18n.json @@ -244,6 +244,7 @@ "aspectRatioButton": "Beeldverhouding", "fullscreenButton": "Volledig scherm activeren", "exitFullscreenButton": "Volledig scherm verlaten", + "alwaysOnTopButton": "Altijd bovenop", "rotationLockButton": "Rotatievergrendeling", "timelineSlider": "Videotijdlijn", "volumeSlider": "Volumeniveau", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index 769c63d0..4a03e62f 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -244,6 +244,7 @@ "aspectRatioButton": "Bildförhållande", "fullscreenButton": "Aktivera helskärm", "exitFullscreenButton": "Avsluta helskärm", + "alwaysOnTopButton": "Alltid överst", "rotationLockButton": "Rotationslås", "timelineSlider": "Videotidslinje", "volumeSlider": "Volymnivå", diff --git a/lib/i18n/strings_zh.i18n.json b/lib/i18n/strings_zh.i18n.json index aa6c0e58..b28f24b8 100644 --- a/lib/i18n/strings_zh.i18n.json +++ b/lib/i18n/strings_zh.i18n.json @@ -244,6 +244,7 @@ "aspectRatioButton": "宽高比", "fullscreenButton": "进入全屏", "exitFullscreenButton": "退出全屏", + "alwaysOnTopButton": "置顶窗口", "rotationLockButton": "旋转锁定", "timelineSlider": "视频时间轴", "volumeSlider": "音量调节", diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index dca91de3..535252d5 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -51,8 +51,10 @@ class DesktopVideoControls extends StatefulWidget { final int audioSyncOffset; final int subtitleSyncOffset; final bool isFullscreen; + final bool isAlwaysOnTop; final VoidCallback? onCycleBoxFitMode; final VoidCallback? onToggleFullscreen; + final VoidCallback? onToggleAlwaysOnTop; final Function(int)? onSwitchVersion; final Function(AudioTrack)? onAudioTrackChanged; final Function(SubtitleTrack)? onSubtitleTrackChanged; @@ -89,8 +91,10 @@ class DesktopVideoControls extends StatefulWidget { this.audioSyncOffset = 0, this.subtitleSyncOffset = 0, this.isFullscreen = false, + this.isAlwaysOnTop = false, this.onCycleBoxFitMode, this.onToggleFullscreen, + this.onToggleAlwaysOnTop, this.onSwitchVersion, this.onAudioTrackChanged, this.onSubtitleTrackChanged, @@ -455,9 +459,11 @@ class DesktopVideoControlsState extends State { subtitleSyncOffset: widget.subtitleSyncOffset, isRotationLocked: false, // Desktop doesn't have rotation lock isFullscreen: widget.isFullscreen, + isAlwaysOnTop: widget.isAlwaysOnTop, serverId: widget.serverId, onCycleBoxFitMode: widget.onCycleBoxFitMode, onToggleFullscreen: widget.onToggleFullscreen, + onToggleAlwaysOnTop: widget.onToggleAlwaysOnTop, onSwitchVersion: widget.onSwitchVersion, onAudioTrackChanged: widget.onAudioTrackChanged, onSubtitleTrackChanged: widget.onSubtitleTrackChanged, diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index 48150408..8e9031a3 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -124,6 +124,7 @@ class _PlexVideoControlsState extends State with WindowListen bool _chaptersLoaded = false; Timer? _hideTimer; bool _isFullscreen = false; + bool _isAlwaysOnTop = false; late final FocusNode _focusNode; KeyboardShortcutsService? _keyboardService; int _seekTimeSmall = 10; // Default, loaded from settings @@ -188,6 +189,7 @@ class _PlexVideoControlsState extends State with WindowListen // Add window listener for tracking fullscreen state (for button icon) if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { windowManager.addListener(this); + _initAlwaysOnTopState(); } // Focus play/pause button on first frame if in keyboard mode WidgetsBinding.instance.addPostFrameCallback((_) { @@ -865,6 +867,25 @@ class _PlexVideoControlsState extends State with WindowListen } } + /// Initialize always-on-top state from window manager (desktop only) + Future _initAlwaysOnTopState() async { + final isOnTop = await windowManager.isAlwaysOnTop(); + if (mounted && isOnTop != _isAlwaysOnTop) { + setState(() { + _isAlwaysOnTop = isOnTop; + }); + } + } + + /// Toggle always-on-top window mode (desktop only) + Future _toggleAlwaysOnTop() async { + if (!PlatformDetector.isMobile(context)) { + _isAlwaysOnTop = !_isAlwaysOnTop; + await windowManager.setAlwaysOnTop(_isAlwaysOnTop); + setState(() {}); + } + } + /// Check if a key is a directional key (arrow keys) bool _isDirectionalKey(LogicalKeyboardKey key) { return key == LogicalKeyboardKey.arrowUp || @@ -1230,8 +1251,10 @@ class _PlexVideoControlsState extends State with WindowListen audioSyncOffset: _audioSyncOffset, subtitleSyncOffset: _subtitleSyncOffset, isFullscreen: _isFullscreen, + isAlwaysOnTop: _isAlwaysOnTop, onCycleBoxFitMode: widget.onCycleBoxFitMode, onToggleFullscreen: _toggleFullscreen, + onToggleAlwaysOnTop: _toggleAlwaysOnTop, onSwitchVersion: _switchMediaVersion, onAudioTrackChanged: widget.onAudioTrackChanged, onSubtitleTrackChanged: widget.onSubtitleTrackChanged, diff --git a/lib/widgets/video_controls/widgets/track_chapter_controls.dart b/lib/widgets/video_controls/widgets/track_chapter_controls.dart index f6c73568..0d6cbc16 100644 --- a/lib/widgets/video_controls/widgets/track_chapter_controls.dart +++ b/lib/widgets/video_controls/widgets/track_chapter_controls.dart @@ -31,9 +31,11 @@ class TrackChapterControls extends StatelessWidget { final int subtitleSyncOffset; final bool isRotationLocked; final bool isFullscreen; + final bool isAlwaysOnTop; final VoidCallback? onCycleBoxFitMode; final VoidCallback? onToggleRotationLock; final VoidCallback? onToggleFullscreen; + final VoidCallback? onToggleAlwaysOnTop; final Function(int)? onSwitchVersion; final Function(AudioTrack)? onAudioTrackChanged; final Function(SubtitleTrack)? onSubtitleTrackChanged; @@ -67,9 +69,11 @@ class TrackChapterControls extends StatelessWidget { required this.isRotationLocked, required this.isFullscreen, required this.serverId, + this.isAlwaysOnTop = false, this.onCycleBoxFitMode, this.onToggleRotationLock, this.onToggleFullscreen, + this.onToggleAlwaysOnTop, this.onSwitchVersion, this.onAudioTrackChanged, this.onSubtitleTrackChanged, @@ -334,6 +338,24 @@ class TrackChapterControls extends StatelessWidget { onPressed: onToggleFullscreen, ), ); + buttonIndex++; + } + + // Always on top button (desktop only, not TV) + if (isDesktop && onToggleAlwaysOnTop != null) { + final currentIndex = buttonIndex; + buttons.add( + _buildTrackButton( + buttonIndex: currentIndex, + icon: Symbols.layers_rounded, + semanticLabel: t.videoControls.alwaysOnTopButton, + isActive: isAlwaysOnTop, + tracks: tracks, + isMobile: isMobile, + isDesktop: isDesktop, + onPressed: onToggleAlwaysOnTop, + ), + ); } return IntrinsicHeight( @@ -352,7 +374,8 @@ class TrackChapterControls extends StatelessWidget { if (availableVersions.length > 1 && onSwitchVersion != null) count++; if (onCycleBoxFitMode != null) count++; if (isMobile && !PlatformDetector.isTV()) count++; // Rotation lock (not on TV) - if (isDesktop) count++; + if (isDesktop) count++; // Fullscreen + if (isDesktop && onToggleAlwaysOnTop != null) count++; // Always on top return count; }