feat: always on top

close #176
This commit is contained in:
edde746
2025-12-19 02:06:33 +01:00
parent a52b62c1b1
commit 289ca393c7
10 changed files with 88 additions and 7 deletions
+29 -6
View File
@@ -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':
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -244,6 +244,7 @@
"aspectRatioButton": "Beeldverhouding",
"fullscreenButton": "Volledig scherm activeren",
"exitFullscreenButton": "Volledig scherm verlaten",
"alwaysOnTopButton": "Altijd bovenop",
"rotationLockButton": "Rotatievergrendeling",
"timelineSlider": "Videotijdlijn",
"volumeSlider": "Volumeniveau",
+1
View File
@@ -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å",
+1
View File
@@ -244,6 +244,7 @@
"aspectRatioButton": "宽高比",
"fullscreenButton": "进入全屏",
"exitFullscreenButton": "退出全屏",
"alwaysOnTopButton": "置顶窗口",
"rotationLockButton": "旋转锁定",
"timelineSlider": "视频时间轴",
"volumeSlider": "音量调节",
@@ -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<DesktopVideoControls> {
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,
@@ -124,6 +124,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> 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<PlexVideoControls> 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<PlexVideoControls> with WindowListen
}
}
/// Initialize always-on-top state from window manager (desktop only)
Future<void> _initAlwaysOnTopState() async {
final isOnTop = await windowManager.isAlwaysOnTop();
if (mounted && isOnTop != _isAlwaysOnTop) {
setState(() {
_isAlwaysOnTop = isOnTop;
});
}
}
/// Toggle always-on-top window mode (desktop only)
Future<void> _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<PlexVideoControls> 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,
@@ -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;
}