diff --git a/lib/main.dart b/lib/main.dart index dec72530..8dd3fb54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -137,7 +137,7 @@ void main() async { void _registerShaderLicenses() { LicenseRegistry.addLicense(() async* { - yield LicenseEntryWithLineBreaks( + yield const LicenseEntryWithLineBreaks( ['Anime4K'], 'MIT License\n' '\n' @@ -162,7 +162,7 @@ void _registerShaderLicenses() { 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ' 'SOFTWARE.', ); - yield LicenseEntryWithLineBreaks( + yield const LicenseEntryWithLineBreaks( ['NVIDIA Image Scaling (NVScaler)'], 'The MIT License (MIT)\n' '\n' @@ -390,9 +390,10 @@ class _SetupScreenState extends State { // Check network connectivity early to fast-path airplane mode. // Timeout guards against connectivity_plus hanging on some Android TV devices after force-close. - final connectivityResult = await Connectivity() - .checkConnectivity() - .timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]); + final connectivityResult = await Connectivity().checkConnectivity().timeout( + const Duration(seconds: 3), + onTimeout: () => [ConnectivityResult.other], + ); final hasNetwork = !connectivityResult.contains(ConnectivityResult.none); if (hasNetwork) { @@ -430,10 +431,7 @@ class _SetupScreenState extends State { _setStatus(t.common.startingOfflineMode); await context.read().ensureInitialized(); if (!mounted) return; - Navigator.pushReplacement( - context, - fadeRoute(const MainScreen(isOfflineMode: true)), - ); + Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); return; } @@ -457,18 +455,12 @@ class _SetupScreenState extends State { downloadProvider.resumeQueuedDownloads(result.firstClient!); }); - Navigator.pushReplacement( - context, - fadeRoute(MainScreen(client: result.firstClient!)), - ); + Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))); } else { _setStatus(t.common.startingOfflineMode); await context.read().ensureInitialized(); if (!mounted) return; - Navigator.pushReplacement( - context, - fadeRoute(const MainScreen(isOfflineMode: true)), - ); + Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); } } catch (e, stackTrace) { appLogger.e('Error during multi-server connection', error: e, stackTrace: stackTrace); @@ -477,10 +469,7 @@ class _SetupScreenState extends State { _setStatus(t.common.startingOfflineMode); await context.read().ensureInitialized(); if (!mounted) return; - Navigator.pushReplacement( - context, - fadeRoute(const MainScreen(isOfflineMode: true)), - ); + Navigator.pushReplacement(context, fadeRoute(const MainScreen(isOfflineMode: true))); } } } @@ -493,9 +482,7 @@ class _SetupScreenState extends State { children: [ // Icon dead-center, matching Android 12+ splash position. // 192dp accounts for the 16% inset in ic_launcher.xml. - Center( - child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288), - ), + Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)), // Status text below center, independent of icon position. Positioned( left: 0, @@ -507,9 +494,9 @@ class _SetupScreenState extends State { _statusMessage, key: ValueKey(_statusMessage), textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)), ), ), ), diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 508b0ad1..dd9ba986 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -113,10 +113,7 @@ class _AuthScreenState extends State { await profileFuture; if (!mounted) return; - Navigator.pushReplacement( - context, - fadeRoute(MainScreen(client: result.firstClient!)), - ); + Navigator.pushReplacement(context, fadeRoute(MainScreen(client: result.firstClient!))); } catch (e) { appLogger.e('Failed to connect to servers', error: e); setState(() { @@ -455,7 +452,7 @@ class _AuthScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 12), side: BorderSide(color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5)), ), - child: Text(t.auth.debugEnterToken, style: TextStyle(fontSize: 12)), + child: Text(t.auth.debugEnterToken, style: const TextStyle(fontSize: 12)), ), ], if (_errorMessage != null) ...[ diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart index 5c51dae4..8d3db5d7 100644 --- a/lib/screens/companion_remote/mobile_remote_screen.dart +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -201,7 +201,7 @@ class _RemoteControlContentState extends State<_RemoteControlContent> { Container( width: 8, height: 8, - decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle), + decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle), ), ], ), @@ -648,7 +648,7 @@ class _SearchBottomSheetState extends State<_SearchBottomSheet> { hintText: t.companionRemote.remote.searchHint, prefixIcon: const Icon(Icons.search), suffixIcon: IconButton(icon: const Icon(Icons.send), onPressed: () => _submit(_controller.text)), - border: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(100))), + border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(100))), ), onSubmitted: _submit, ), diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 196be93b..e0ca4882 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -258,7 +258,6 @@ class _DiscoverScreenState extends State _loadContent(); } - /// Handle key events for the hero section late final _handleHeroKeyEvent = dpadKeyHandler( onDown: () { @@ -285,7 +284,6 @@ class _DiscoverScreenState extends State }, ); - @override void dispose() { _hiddenLibrariesProvider?.removeListener(_onHiddenLibrariesChanged); @@ -781,12 +779,18 @@ class _DiscoverScreenState extends State PopupMenuItem( value: 'switch_profile', child: Row( - children: [AppIcon(Symbols.people_rounded, fill: 1), SizedBox(width: 8), Text(t.discover.switchProfile)], + children: [ + AppIcon(Symbols.people_rounded, fill: 1), + const SizedBox(width: 8), + Text(t.discover.switchProfile), + ], ), ), PopupMenuItem( value: 'logout', - child: Row(children: [AppIcon(Symbols.logout_rounded, fill: 1), SizedBox(width: 8), Text(t.common.logout)]), + child: Row( + children: [AppIcon(Symbols.logout_rounded, fill: 1), const SizedBox(width: 8), Text(t.common.logout)], + ), ), ], ).then((value) { @@ -838,14 +842,11 @@ class _DiscoverScreenState extends State onNavigateLeft: _navigateToSidebar, onNavigateDown: _focusContentFromAppBar, actions: [ - FocusableAction( - icon: Symbols.refresh_rounded, - iconColor: Colors.white, - onPressed: _loadContent, - ), + FocusableAction(icon: Symbols.refresh_rounded, iconColor: Colors.white, onPressed: _loadContent), // Watch Together FocusableAction( - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())), + onPressed: () => + Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())), child: Stack( children: [ IconButton( @@ -854,7 +855,10 @@ class _DiscoverScreenState extends State fill: watchTogether.isInSession ? 1 : 0, color: watchTogether.isInSession ? Theme.of(context).colorScheme.primary : Colors.white, ), - onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WatchTogetherScreen())), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const WatchTogetherScreen()), + ), tooltip: 'Watch Together', ), if (watchTogether.isInSession && watchTogether.participantCount > 1) @@ -886,7 +890,10 @@ class _DiscoverScreenState extends State if (isDesktop) { RemoteSessionDialog.show(context); } else { - Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen())); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MobileRemoteScreen()), + ); } }, child: Stack( @@ -895,13 +902,18 @@ class _DiscoverScreenState extends State icon: AppIcon( Symbols.phone_android_rounded, fill: companionRemote.isConnected ? 1 : 0, - color: companionRemote.isConnected ? Theme.of(context).colorScheme.primary : Colors.white, + color: companionRemote.isConnected + ? Theme.of(context).colorScheme.primary + : Colors.white, ), onPressed: () { if (isDesktop) { RemoteSessionDialog.show(context); } else { - Navigator.push(context, MaterialPageRoute(builder: (context) => MobileRemoteScreen())); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MobileRemoteScreen()), + ); } }, tooltip: t.companionRemote.title, @@ -913,10 +925,10 @@ class _DiscoverScreenState extends State child: Container( width: 8, height: 8, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, - border: const Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)), + border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 1)), ), ), ), @@ -944,7 +956,7 @@ class _DiscoverScreenState extends State child: Row( children: [ AppIcon(Symbols.people_rounded, fill: 1), - SizedBox(width: 8), + const SizedBox(width: 8), Text(t.discover.switchProfile), ], ), @@ -954,7 +966,7 @@ class _DiscoverScreenState extends State child: Row( children: [ AppIcon(Symbols.logout_rounded, fill: 1), - SizedBox(width: 8), + const SizedBox(width: 8), Text(t.common.logout), ], ), @@ -1097,11 +1109,11 @@ class _DiscoverScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey), - SizedBox(height: 16), + const AppIcon(Symbols.movie_rounded, fill: 1, size: 64, color: Colors.grey), + const SizedBox(height: 16), Text(t.discover.noContentAvailable), - SizedBox(height: 8), - Text(t.discover.addMediaToLibraries, style: TextStyle(color: Colors.grey)), + const SizedBox(height: 8), + Text(t.discover.addMediaToLibraries, style: const TextStyle(color: Colors.grey)), ], ), ), @@ -1575,7 +1587,7 @@ class _DiscoverScreenState extends State ] else Text( t.common.play, - style: TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600), + style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w600), ), ], ), diff --git a/lib/screens/focusable_detail_screen_mixin.dart b/lib/screens/focusable_detail_screen_mixin.dart index a2ffa77f..d58825b6 100644 --- a/lib/screens/focusable_detail_screen_mixin.dart +++ b/lib/screens/focusable_detail_screen_mixin.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../focus/focusable_action_bar.dart'; import '../focus/input_mode_tracker.dart'; -import '../focus/key_event_utils.dart'; import '../mixins/grid_focus_node_mixin.dart'; import '../providers/settings_provider.dart'; import '../utils/grid_size_calculator.dart'; diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index c9fff089..ac0c288b 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../../services/plex_client.dart'; @@ -15,11 +14,8 @@ import '../widgets/focusable_media_card.dart'; import '../widgets/media_grid_delegate.dart'; import '../widgets/desktop_app_bar.dart'; import '../widgets/overlay_sheet.dart'; -import 'package:flutter/services.dart'; -import '../focus/dpad_navigator.dart'; import '../focus/focusable_action_bar.dart'; import '../focus/input_mode_tracker.dart'; -import '../focus/key_event_utils.dart'; import '../mixins/grid_focus_node_mixin.dart'; import 'libraries/sort_bottom_sheet.dart'; import 'libraries/state_messages.dart'; diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index 7e1e24ae..a16db00c 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import '../../focus/dpad_navigator.dart'; import '../../focus/focusable_action_bar.dart'; import '../../i18n/strings.g.dart'; import '../../models/livetv_channel.dart'; diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index db9cda78..a0831cf7 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -533,7 +533,7 @@ class GuideTabState extends State { children: [ Row( children: [ - SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight), + const SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight), Expanded( child: SingleChildScrollView( controller: _headerHorizontalController, diff --git a/lib/screens/profile/pin_entry_dialog.dart b/lib/screens/profile/pin_entry_dialog.dart index a8056592..e10e3c89 100644 --- a/lib/screens/profile/pin_entry_dialog.dart +++ b/lib/screens/profile/pin_entry_dialog.dart @@ -365,7 +365,7 @@ class _TvPinInputState extends State<_TvPinInput> { _digits[index] = digit; _activeIndex = index; _mobileControllers[index].text = digit.toString(); - _mobileControllers[index].selection = TextSelection.collapsed(offset: 1); + _mobileControllers[index].selection = const TextSelection.collapsed(offset: 1); }); if (index < 3) { @@ -436,12 +436,12 @@ class _TvPinInputState extends State<_TvPinInput> { maxLength: 2, // allow overwrite obscureText: true, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), - decoration: InputDecoration( + decoration: const InputDecoration( counterText: '', border: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)), + borderRadius: BorderRadius.all(Radius.circular(FocusTheme.defaultBorderRadius)), ), - contentPadding: const EdgeInsets.symmetric(vertical: 14), + contentPadding: EdgeInsets.symmetric(vertical: 14), ), inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) => _onMobileDigitChanged(i, value), diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 9ecc5f59..aba5376e 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -235,16 +235,16 @@ class _SearchScreenState extends State with Refreshable, FullRefre : null, filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(100)), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(100)), borderSide: BorderSide.none, ), - enabledBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(100)), + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(100)), borderSide: BorderSide.none, ), - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(100)), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(100)), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 13ad8091..2fc85163 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -59,7 +59,8 @@ class _SeasonDetailScreenState extends State bool _suppressNextBackKeyUp = false; bool _routeSubscribed = false; - String _toGlobalKey(String ratingKey, {String? serverId}) => buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey); + String _toGlobalKey(String ratingKey, {String? serverId}) => + buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey); // WatchStateAware: watch all episode ratingKeys @override @@ -364,14 +365,18 @@ class _EpisodeCardState extends State<_EpisodeCard> { ); return Row( children: [ - if (widget.episode.duration != null) Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle), + if (widget.episode.duration != null) + Text(formatDurationTimestamp(Duration(milliseconds: widget.episode.duration!)), style: mutedStyle), if (widget.episode.originallyAvailableAt != null) ...[ dot, Text(formatFullDate(widget.episode.originallyAvailableAt!), style: mutedStyle), ], if (widget.episode.userRating != null && widget.episode.userRating! > 0) ...[ dot, - Padding(padding: const EdgeInsets.only(top: 2), child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber)), + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon(Symbols.star_rounded, size: 12, fill: 1, color: Colors.amber), + ), const SizedBox(width: 2), Text( (widget.episode.userRating! / 2) == (widget.episode.userRating! / 2).truncateToDouble() diff --git a/lib/screens/settings/logs_screen.dart b/lib/screens/settings/logs_screen.dart index 8c83ccce..edbabbe5 100644 --- a/lib/screens/settings/logs_screen.dart +++ b/lib/screens/settings/logs_screen.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:flutter/services.dart'; import 'package:logger/logger.dart'; diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index ef715099..e6c51f48 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -51,10 +51,9 @@ class PlexAuthService { static Future create() async { final storage = await StorageService.getInstance(); - final dio = Dio(BaseOptions( - connectTimeout: ConnectionTimeouts.plexTvConnect, - receiveTimeout: ConnectionTimeouts.plexTvReceive, - )); + final dio = Dio( + BaseOptions(connectTimeout: ConnectionTimeouts.plexTvConnect, receiveTimeout: ConnectionTimeouts.plexTvReceive), + ); // Get or create client identifier String? clientIdentifier = storage.getClientIdentifier(); @@ -276,7 +275,7 @@ class PlexServer { factory PlexServer.fromJson(Map json) { // Validate required fields first if (!_isValidServerJson(json)) { - throw FormatException( + throw const FormatException( 'Invalid server data: missing required fields (name, clientIdentifier, accessToken, or connections)', ); } @@ -302,7 +301,7 @@ class PlexServer { // If no valid connections were parsed, this server is unusable if (connections.isEmpty) { - throw FormatException('Server has no valid connections'); + throw const FormatException('Server has no valid connections'); } DateTime? lastSeenAt; @@ -863,7 +862,7 @@ class PlexConnection { factory PlexConnection.fromJson(Map json) { // Validate required fields if (!_isValidConnectionJson(json)) { - throw FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)'); + throw const FormatException('Invalid connection data: missing required fields (protocol, address, port, or uri)'); } return PlexConnection( diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 4b830db9..3fd86334 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -399,27 +399,27 @@ class SettingsService extends BaseSharedPreferencesService { // HotKey Objects (New implementation) Map getDefaultKeyboardHotkeys() { return { - 'play_pause': HotKey(key: PhysicalKeyboardKey.space), - 'volume_up': HotKey(key: PhysicalKeyboardKey.arrowUp), - 'volume_down': HotKey(key: PhysicalKeyboardKey.arrowDown), - 'seek_forward': HotKey(key: PhysicalKeyboardKey.arrowRight), - 'seek_backward': HotKey(key: PhysicalKeyboardKey.arrowLeft), - 'seek_forward_large': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]), - 'seek_backward_large': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]), - 'fullscreen_toggle': HotKey(key: PhysicalKeyboardKey.keyF), - 'mute_toggle': HotKey(key: PhysicalKeyboardKey.keyM), - 'subtitle_toggle': HotKey(key: PhysicalKeyboardKey.keyS), - 'audio_track_next': HotKey(key: PhysicalKeyboardKey.keyA), - 'subtitle_track_next': HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]), - 'chapter_next': HotKey(key: PhysicalKeyboardKey.keyN), - 'chapter_previous': HotKey(key: PhysicalKeyboardKey.keyP), - 'speed_increase': HotKey(key: PhysicalKeyboardKey.equal), - 'speed_decrease': HotKey(key: PhysicalKeyboardKey.minus), - 'speed_reset': HotKey(key: PhysicalKeyboardKey.keyR), - 'sub_seek_next': HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]), - 'sub_seek_prev': HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]), - 'shader_toggle': HotKey(key: PhysicalKeyboardKey.keyG), - 'skip_marker': HotKey(key: PhysicalKeyboardKey.enter), + 'play_pause': const HotKey(key: PhysicalKeyboardKey.space), + 'volume_up': const HotKey(key: PhysicalKeyboardKey.arrowUp), + 'volume_down': const HotKey(key: PhysicalKeyboardKey.arrowDown), + 'seek_forward': const HotKey(key: PhysicalKeyboardKey.arrowRight), + 'seek_backward': const HotKey(key: PhysicalKeyboardKey.arrowLeft), + 'seek_forward_large': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.shift]), + 'seek_backward_large': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.shift]), + 'fullscreen_toggle': const HotKey(key: PhysicalKeyboardKey.keyF), + 'mute_toggle': const HotKey(key: PhysicalKeyboardKey.keyM), + 'subtitle_toggle': const HotKey(key: PhysicalKeyboardKey.keyS), + 'audio_track_next': const HotKey(key: PhysicalKeyboardKey.keyA), + 'subtitle_track_next': const HotKey(key: PhysicalKeyboardKey.keyS, modifiers: [HotKeyModifier.shift]), + 'chapter_next': const HotKey(key: PhysicalKeyboardKey.keyN), + 'chapter_previous': const HotKey(key: PhysicalKeyboardKey.keyP), + 'speed_increase': const HotKey(key: PhysicalKeyboardKey.equal), + 'speed_decrease': const HotKey(key: PhysicalKeyboardKey.minus), + 'speed_reset': const HotKey(key: PhysicalKeyboardKey.keyR), + 'sub_seek_next': const HotKey(key: PhysicalKeyboardKey.arrowRight, modifiers: [HotKeyModifier.control]), + 'sub_seek_prev': const HotKey(key: PhysicalKeyboardKey.arrowLeft, modifiers: [HotKeyModifier.control]), + 'shader_toggle': const HotKey(key: PhysicalKeyboardKey.keyG), + 'skip_marker': const HotKey(key: PhysicalKeyboardKey.enter), }; } diff --git a/lib/theme/mono_theme.dart b/lib/theme/mono_theme.dart index b2096888..dffff939 100644 --- a/lib/theme/mono_theme.dart +++ b/lib/theme/mono_theme.dart @@ -95,7 +95,7 @@ ThemeData monoTheme({required bool dark, bool oled = false}) { color: c.surface, elevation: 0, margin: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: const BorderRadius.all(Radius.circular(14))), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14))), ), inputDecorationTheme: InputDecorationTheme( filled: true, diff --git a/lib/widgets/video_controls/desktop_video_controls.dart b/lib/widgets/video_controls/desktop_video_controls.dart index a74fc2b2..7b905984 100644 --- a/lib/widgets/video_controls/desktop_video_controls.dart +++ b/lib/widgets/video_controls/desktop_video_controls.dart @@ -453,7 +453,7 @@ class DesktopVideoControlsState extends State { const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))), + decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))), child: Text( t.liveTv.live, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12), diff --git a/lib/widgets/video_controls/mobile_video_controls.dart b/lib/widgets/video_controls/mobile_video_controls.dart index 2973f989..f59584cd 100644 --- a/lib/widgets/video_controls/mobile_video_controls.dart +++ b/lib/widgets/video_controls/mobile_video_controls.dart @@ -199,7 +199,7 @@ class MobileVideoControls extends StatelessWidget { children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: Colors.red, borderRadius: const BorderRadius.all(Radius.circular(4))), + decoration: const BoxDecoration(color: Colors.red, borderRadius: BorderRadius.all(Radius.circular(4))), child: Text( t.liveTv.live, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12), diff --git a/lib/widgets/video_controls/sheets/chapter_sheet.dart b/lib/widgets/video_controls/sheets/chapter_sheet.dart index 8b6edbf9..88556232 100644 --- a/lib/widgets/video_controls/sheets/chapter_sheet.dart +++ b/lib/widgets/video_controls/sheets/chapter_sheet.dart @@ -35,7 +35,6 @@ class ChapterSheet extends StatefulWidget { } class _ChapterSheetState extends State { - /// Get the PlexClient for chapters, or null if unavailable (offline mode) PlexClient? _tryGetClientForChapters(BuildContext context) { if (widget.serverId == null) return null; @@ -49,55 +48,53 @@ class _ChapterSheetState extends State { @override Widget build(BuildContext context) { return StreamBuilder( - stream: widget.player.streams.position, - initialData: widget.player.state.position, - builder: (context, positionSnapshot) { - final currentPosition = positionSnapshot.data ?? Duration.zero; - final currentPositionMs = currentPosition.inMilliseconds; + stream: widget.player.streams.position, + initialData: widget.player.state.position, + builder: (context, positionSnapshot) { + final currentPosition = positionSnapshot.data ?? Duration.zero; + final currentPositionMs = currentPosition.inMilliseconds; - // Find the current chapter based on position - int? currentChapterIndex; - for (int i = 0; i < widget.chapters.length; i++) { - final chapter = widget.chapters[i]; - final startMs = chapter.startTimeOffset ?? 0; - final endMs = - chapter.endTimeOffset ?? - (i < widget.chapters.length - 1 - ? widget.chapters[i + 1].startTimeOffset ?? 0 - : double.maxFinite.toInt()); + // Find the current chapter based on position + int? currentChapterIndex; + for (int i = 0; i < widget.chapters.length; i++) { + final chapter = widget.chapters[i]; + final startMs = chapter.startTimeOffset ?? 0; + final endMs = + chapter.endTimeOffset ?? + (i < widget.chapters.length - 1 ? widget.chapters[i + 1].startTimeOffset ?? 0 : double.maxFinite.toInt()); - if (currentPositionMs >= startMs && currentPositionMs < endMs) { - currentChapterIndex = i; - break; - } + if (currentPositionMs >= startMs && currentPositionMs < endMs) { + currentChapterIndex = i; + break; } + } - Widget content; - if (!widget.chaptersLoaded) { - content = const Center(child: CircularProgressIndicator()); - } else if (widget.chapters.isEmpty) { - content = Center( - child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)), - ); - } else { - content = ListView.builder( - itemCount: widget.chapters.length, - itemBuilder: (context, index) { - final chapter = widget.chapters[index]; - final isCurrentChapter = currentChapterIndex == index; + Widget content; + if (!widget.chaptersLoaded) { + content = const Center(child: CircularProgressIndicator()); + } else if (widget.chapters.isEmpty) { + content = Center( + child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)), + ); + } else { + content = ListView.builder( + itemCount: widget.chapters.length, + itemBuilder: (context, index) { + final chapter = widget.chapters[index]; + final isCurrentChapter = currentChapterIndex == index; - // Get local file path for offline chapter thumbnails - final localThumbPath = widget.serverId != null && chapter.thumb != null - ? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!) - : null; + // Get local file path for offline chapter thumbnails + final localThumbPath = widget.serverId != null && chapter.thumb != null + ? DownloadStorageService.instance.getArtworkPathSync(widget.serverId!, chapter.thumb!) + : null; - return FocusableListTile( - leading: chapter.thumb != null - ? SizedBox( - width: 60, - height: 34, - child: Stack( - children: [ + return FocusableListTile( + leading: chapter.thumb != null + ? SizedBox( + width: 60, + height: 34, + child: Stack( + children: [ ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(4)), child: PlexOptimizedImage.thumb( @@ -114,48 +111,48 @@ class _ChapterSheetState extends State { if (isCurrentChapter) Positioned.fill( child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)), ), ), ), - ], - ), - ) - : null, - title: Text( - chapter.label, - style: TextStyle( - color: isCurrentChapter ? Colors.blue : null, - fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal, - ), + ], + ), + ) + : null, + title: Text( + chapter.label, + style: TextStyle( + color: isCurrentChapter ? Colors.blue : null, + fontWeight: isCurrentChapter ? FontWeight.bold : FontWeight.normal, ), - subtitle: Text( - formatDurationTimestamp(chapter.startTime), - style: TextStyle( - color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted, - fontSize: 12, - ), + ), + subtitle: Text( + formatDurationTimestamp(chapter.startTime), + style: TextStyle( + color: isCurrentChapter ? Colors.blue.withValues(alpha: 0.7) : tokens(context).textMuted, + fontSize: 12, ), - trailing: isCurrentChapter - ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) - : null, - onTap: () { - widget.player.seek(chapter.startTime); - OverlaySheetController.of(context).close(); - }, - ); - }, - ); - } - - return BaseVideoControlSheet( - title: t.videoControls.chapters, - icon: Symbols.video_library_rounded, - child: content, + ), + trailing: isCurrentChapter + ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) + : null, + onTap: () { + widget.player.seek(chapter.startTime); + OverlaySheetController.of(context).close(); + }, + ); + }, ); - }, - ); + } + + return BaseVideoControlSheet( + title: t.videoControls.chapters, + icon: Symbols.video_library_rounded, + child: content, + ); + }, + ); } } diff --git a/lib/widgets/video_controls/sheets/queue_sheet.dart b/lib/widgets/video_controls/sheets/queue_sheet.dart index 4a3a2228..ec65b752 100644 --- a/lib/widgets/video_controls/sheets/queue_sheet.dart +++ b/lib/widgets/video_controls/sheets/queue_sheet.dart @@ -62,9 +62,7 @@ class QueueSheet extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: isCurrent - ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) - : null, + trailing: isCurrent ? const AppIcon(Symbols.play_circle_rounded, fill: 1, color: Colors.blue) : null, onTap: () { onItemSelected(item); OverlaySheetController.of(context).close(); @@ -74,11 +72,7 @@ class QueueSheet extends StatelessWidget { ); } - return BaseVideoControlSheet( - title: t.videoControls.queue, - icon: Symbols.queue_music_rounded, - child: content, - ); + return BaseVideoControlSheet(title: t.videoControls.queue, icon: Symbols.queue_music_rounded, child: content); }, ); } @@ -109,9 +103,9 @@ class QueueSheet extends StatelessWidget { if (isCurrent) Positioned.fill( child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - border: const Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + border: Border.fromBorderSide(BorderSide(color: Colors.blue, width: 2)), ), ), ), diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index b5853d7b..25d998cf 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -1288,7 +1288,7 @@ class _PlexVideoControlsState extends State with WindowListen child: Row( mainAxisSize: MainAxisSize.min, children: [ - AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16), + const AppIcon(Symbols.fast_forward_rounded, fill: 1, color: Colors.white, size: 16), const SizedBox(width: 4), const Text( '2x', @@ -1565,7 +1565,9 @@ class _PlexVideoControlsState extends State with WindowListen // On Windows/Linux with navigation off, ESC only exits fullscreen — // never exits the player. Consume all back key events and check // actual window state asynchronously. - if (!_videoPlayerNavigationEnabled && (Platform.isWindows || Platform.isLinux) && event.logicalKey.isBackKey) { + if (!_videoPlayerNavigationEnabled && + (Platform.isWindows || Platform.isLinux) && + event.logicalKey.isBackKey) { if (event is KeyUpEvent) { _exitFullscreenIfNeeded(); } diff --git a/lib/widgets/video_controls/widgets/timeline_slider.dart b/lib/widgets/video_controls/widgets/timeline_slider.dart index c2362809..ac3ebf05 100644 --- a/lib/widgets/video_controls/widgets/timeline_slider.dart +++ b/lib/widgets/video_controls/widgets/timeline_slider.dart @@ -242,25 +242,25 @@ class _TimelineSliderState extends State { overlayShape: const RoundSliderOverlayShape(overlayRadius: 12), ), child: Semantics( - label: t.videoControls.timelineSlider, - slider: true, - child: Slider( - value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0, - min: 0.0, - max: widget.duration.inMilliseconds.toDouble(), - onChanged: (value) { - setState(() => _dragValue = value); - widget.onSeek(Duration(milliseconds: value.toInt())); - }, - onChangeEnd: (value) { - setState(() => _dragValue = null); - widget.onSeekEnd(Duration(milliseconds: value.toInt())); - }, - activeColor: Colors.white, - inactiveColor: Colors.transparent, + label: t.videoControls.timelineSlider, + slider: true, + child: Slider( + value: widget.duration.inMilliseconds > 0 ? widget.position.inMilliseconds.toDouble() : 0.0, + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + onChanged: (value) { + setState(() => _dragValue = value); + widget.onSeek(Duration(milliseconds: value.toInt())); + }, + onChangeEnd: (value) { + setState(() => _dragValue = null); + widget.onSeekEnd(Duration(milliseconds: value.toInt())); + }, + activeColor: Colors.white, + inactiveColor: Colors.transparent, + ), ), ), - ), ), // Chapter marker indicators if (widget.chaptersLoaded && widget.chapters.isNotEmpty && widget.duration.inMilliseconds > 0)