From a6f0a0b6c0b8291ea5bcd604236d4b394423e5ed Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:53:20 +0100 Subject: [PATCH] refactor: format & deduplicate --- lib/mpv/src/models/audio_device.dart | 5 +- lib/mpv/src/models/audio_track.dart | 4 +- lib/mpv/src/models/media.dart | 6 +- lib/mpv/src/models/subtitle_track.dart | 6 +- lib/mpv/src/models/track_selection.dart | 10 +- lib/mpv/src/models/tracks.dart | 10 +- lib/mpv/src/player/player_native.dart | 53 ++++---- lib/screens/auth_screen.dart | 40 +++--- .../library_tabs/library_browse_tab.dart | 68 ++++------ .../library_tabs/library_playlists_tab.dart | 31 ++--- .../library_tabs/library_recommended_tab.dart | 1 + lib/screens/logs_screen.dart | 84 ++++++------ lib/screens/media_detail_screen.dart | 1 - lib/screens/search_screen.dart | 1 - lib/screens/season_detail_screen.dart | 1 - lib/screens/settings_screen.dart | 1 - lib/screens/video_player_screen.dart | 127 ++++++++++------- .../playback_initialization_service.dart | 4 +- lib/services/storage_service.dart | 33 ++--- lib/services/update_service.dart | 22 +-- lib/services/video_filter_manager.dart | 29 ++-- lib/widgets/filters_bottom_sheet.dart | 1 - lib/widgets/media_context_menu.dart | 2 +- .../video_controls/mobile_video_controls.dart | 128 ++++++++---------- .../sheets/track_selection_sheet.dart | 3 +- .../video_controls/video_controls.dart | 91 +++++++------ 26 files changed, 361 insertions(+), 401 deletions(-) diff --git a/lib/mpv/src/models/audio_device.dart b/lib/mpv/src/models/audio_device.dart index 5fc157d0..be5dca7d 100644 --- a/lib/mpv/src/models/audio_device.dart +++ b/lib/mpv/src/models/audio_device.dart @@ -6,10 +6,7 @@ class AudioDevice { /// Human-readable description of the device. final String description; - const AudioDevice({ - required this.name, - this.description = '', - }); + const AudioDevice({required this.name, this.description = ''}); /// Default/auto audio device. static const auto = AudioDevice(name: 'auto', description: 'Auto'); diff --git a/lib/mpv/src/models/audio_track.dart b/lib/mpv/src/models/audio_track.dart index b1efe622..11e2b565 100644 --- a/lib/mpv/src/models/audio_track.dart +++ b/lib/mpv/src/models/audio_track.dart @@ -61,9 +61,7 @@ class AudioTrack { @override bool operator ==(Object other) => identical(this, other) || - other is AudioTrack && - runtimeType == other.runtimeType && - id == other.id; + other is AudioTrack && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; diff --git a/lib/mpv/src/models/media.dart b/lib/mpv/src/models/media.dart index 2d395155..569896bf 100644 --- a/lib/mpv/src/models/media.dart +++ b/lib/mpv/src/models/media.dart @@ -9,11 +9,7 @@ class Media { /// Optional start position for playback. final Duration? start; - const Media( - this.uri, { - this.headers, - this.start, - }); + const Media(this.uri, {this.headers, this.start}); @override String toString() => 'Media($uri)'; diff --git a/lib/mpv/src/models/subtitle_track.dart b/lib/mpv/src/models/subtitle_track.dart index 0c75b2dc..24e1fc52 100644 --- a/lib/mpv/src/models/subtitle_track.dart +++ b/lib/mpv/src/models/subtitle_track.dart @@ -36,11 +36,7 @@ class SubtitleTrack { }); /// Create a subtitle track from an external URI. - factory SubtitleTrack.uri( - String uri, { - String? title, - String? language, - }) { + factory SubtitleTrack.uri(String uri, {String? title, String? language}) { return SubtitleTrack( id: 'external:$uri', title: title, diff --git a/lib/mpv/src/models/track_selection.dart b/lib/mpv/src/models/track_selection.dart index 284b46e1..1a9cbe3e 100644 --- a/lib/mpv/src/models/track_selection.dart +++ b/lib/mpv/src/models/track_selection.dart @@ -9,16 +9,10 @@ class TrackSelection { /// Currently selected subtitle track. final SubtitleTrack? subtitle; - const TrackSelection({ - this.audio, - this.subtitle, - }); + const TrackSelection({this.audio, this.subtitle}); /// Creates a copy with the given fields replaced. - TrackSelection copyWith({ - AudioTrack? audio, - SubtitleTrack? subtitle, - }) { + TrackSelection copyWith({AudioTrack? audio, SubtitleTrack? subtitle}) { return TrackSelection( audio: audio ?? this.audio, subtitle: subtitle ?? this.subtitle, diff --git a/lib/mpv/src/models/tracks.dart b/lib/mpv/src/models/tracks.dart index 18fe3607..0892600b 100644 --- a/lib/mpv/src/models/tracks.dart +++ b/lib/mpv/src/models/tracks.dart @@ -9,16 +9,10 @@ class Tracks { /// Available subtitle tracks. final List subtitle; - const Tracks({ - this.audio = const [], - this.subtitle = const [], - }); + const Tracks({this.audio = const [], this.subtitle = const []}); /// Creates a copy with the given fields replaced. - Tracks copyWith({ - List? audio, - List? subtitle, - }) { + Tracks copyWith({List? audio, List? subtitle}) { return Tracks( audio: audio ?? this.audio, subtitle: subtitle ?? this.subtitle, diff --git a/lib/mpv/src/player/player_native.dart b/lib/mpv/src/player/player_native.dart index ee845ca6..d1db58b0 100644 --- a/lib/mpv/src/player/player_native.dart +++ b/lib/mpv/src/player/player_native.dart @@ -205,24 +205,28 @@ class PlayerNative implements Player { final id = track['id']?.toString() ?? ''; if (type == 'audio') { - audioTracks.add(AudioTrack( - id: id, - title: track['title'] as String?, - language: track['lang'] as String?, - codec: track['codec'] as String?, - channels: (track['demux-channel-count'] as num?)?.toInt(), - sampleRate: (track['demux-samplerate'] as num?)?.toInt(), - isDefault: track['default'] as bool? ?? false, - )); + audioTracks.add( + AudioTrack( + id: id, + title: track['title'] as String?, + language: track['lang'] as String?, + codec: track['codec'] as String?, + channels: (track['demux-channel-count'] as num?)?.toInt(), + sampleRate: (track['demux-samplerate'] as num?)?.toInt(), + isDefault: track['default'] as bool? ?? false, + ), + ); } else if (type == 'sub') { - subtitleTracks.add(SubtitleTrack( - id: id, - title: track['title'] as String?, - language: track['lang'] as String?, - codec: track['codec'] as String?, - isExternal: track['external'] as bool? ?? false, - uri: track['external-filename'] as String?, - )); + subtitleTracks.add( + SubtitleTrack( + id: id, + title: track['title'] as String?, + language: track['lang'] as String?, + codec: track['codec'] as String?, + isExternal: track['external'] as bool? ?? false, + uri: track['external-filename'] as String?, + ), + ); } } @@ -235,9 +239,9 @@ class PlayerNative implements Player { if (id != null && id != 'no') { selectedTrack = _state.tracks.audio.cast().firstWhere( - (t) => t?.id == id, - orElse: () => null, - ); + (t) => t?.id == id, + orElse: () => null, + ); } _state = _state.copyWith( @@ -251,11 +255,10 @@ class PlayerNative implements Player { SubtitleTrack? selectedTrack; if (id != null && id != 'no') { - selectedTrack = - _state.tracks.subtitle.cast().firstWhere( - (t) => t?.id == id, - orElse: () => null, - ); + selectedTrack = _state.tracks.subtitle.cast().firstWhere( + (t) => t?.id == id, + orElse: () => null, + ); } _state = _state.copyWith( diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 65f9eefc..744bd6ea 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -349,17 +349,7 @@ class _AuthScreenState extends State { ), ), ), - const SizedBox(height: 24), - OutlinedButton( - onPressed: _retryAuthentication, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 24, - ), - ), - child: Text(t.auth.retry), - ), + _buildRetryButton(), ] else ...[ // Browser auth flow - show spinner and waiting message const Center(child: CircularProgressIndicator()), @@ -369,17 +359,7 @@ class _AuthScreenState extends State { textAlign: TextAlign.center, style: const TextStyle(color: Colors.grey), ), - const SizedBox(height: 24), - OutlinedButton( - onPressed: _retryAuthentication, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 24, - ), - ), - child: Text(t.auth.retry), - ), + _buildRetryButton(), ], ] else ...[ // Initial state buttons @@ -531,4 +511,20 @@ class _AuthScreenState extends State { ), ); } + + Widget _buildRetryButton() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + OutlinedButton( + onPressed: _retryAuthentication, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + ), + child: Text(t.auth.retry), + ), + ], + ); + } } diff --git a/lib/screens/library_tabs/library_browse_tab.dart b/lib/screens/library_tabs/library_browse_tab.dart index b1a0b373..311a75e9 100644 --- a/lib/screens/library_tabs/library_browse_tab.dart +++ b/lib/screens/library_tabs/library_browse_tab.dart @@ -181,12 +181,7 @@ class _LibraryBrowseTabState extends State // Load items await _loadItems(); } catch (e) { - if (currentRequestId != _requestId) return; - - setState(() { - _errorMessage = _getErrorMessage(e); - _isLoading = false; - }); + _handleLoadError(e, currentRequestId); } } @@ -255,15 +250,19 @@ class _LibraryBrowseTabState extends State _isLoading = false; }); } catch (e) { - if (currentRequestId != _requestId) return; - - setState(() { - _errorMessage = _getErrorMessage(e); - _isLoading = false; - }); + _handleLoadError(e, currentRequestId); } } + void _handleLoadError(dynamic error, int currentRequestId) { + if (currentRequestId != _requestId) return; + + setState(() { + _errorMessage = _getErrorMessage(error); + _isLoading = false; + }); + } + String _getDefaultGrouping() { final type = widget.library.type.toLowerCase(); if (type == 'show') { @@ -591,21 +590,7 @@ class _LibraryBrowseTabState extends State padding: const EdgeInsets.all(8), itemCount: _items.length + (_hasMoreItems && _isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (index >= _items.length) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), - ); - } - final item = _items[index]; - return MediaCard( - key: Key(item.ratingKey), - item: item, - onRefresh: updateItem, - focusNode: index == 0 ? _firstItemFocusNode : null, - ); - }, + itemBuilder: (context, index) => _buildMediaCardItem(index), ), ); } else { @@ -624,18 +609,7 @@ class _LibraryBrowseTabState extends State ), itemCount: _items.length + (_hasMoreItems && _isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (index >= _items.length) { - return const Center(child: CircularProgressIndicator()); - } - final item = _items[index]; - return MediaCard( - key: Key(item.ratingKey), - item: item, - onRefresh: updateItem, - focusNode: index == 0 ? _firstItemFocusNode : null, - ); - }, + itemBuilder: (context, index) => _buildMediaCardItem(index), ), ); } @@ -643,4 +617,20 @@ class _LibraryBrowseTabState extends State ), ); } + + Widget _buildMediaCardItem(int index) { + if (index >= _items.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + final item = _items[index]; + return MediaCard( + key: Key(item.ratingKey), + item: item, + onRefresh: updateItem, + focusNode: index == 0 ? _firstItemFocusNode : null, + ); + } } diff --git a/lib/screens/library_tabs/library_playlists_tab.dart b/lib/screens/library_tabs/library_playlists_tab.dart index eb8ffc6d..e65a9981 100644 --- a/lib/screens/library_tabs/library_playlists_tab.dart +++ b/lib/screens/library_tabs/library_playlists_tab.dart @@ -75,15 +75,8 @@ class _LibraryPlaylistsTabState return ListView.builder( padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), itemCount: items.length, - itemBuilder: (context, index) { - final playlist = items[index]; - return MediaCard( - key: Key(playlist.ratingKey), - item: playlist, - onListRefresh: loadItems, - focusNode: index == 0 ? _firstItemFocusNode : null, - ); - }, + itemBuilder: (context, index) => + _buildPlaylistItem(items[index], index), ); } else { return GridView.builder( @@ -98,18 +91,20 @@ class _LibraryPlaylistsTabState mainAxisSpacing: 0, ), itemCount: items.length, - itemBuilder: (context, index) { - final playlist = items[index]; - return MediaCard( - key: Key(playlist.ratingKey), - item: playlist, - onListRefresh: loadItems, - focusNode: index == 0 ? _firstItemFocusNode : null, - ); - }, + itemBuilder: (context, index) => + _buildPlaylistItem(items[index], index), ); } }, ); } + + Widget _buildPlaylistItem(PlexPlaylist playlist, int index) { + return MediaCard( + key: Key(playlist.ratingKey), + item: playlist, + onListRefresh: loadItems, + focusNode: index == 0 ? _firstItemFocusNode : null, + ); + } } diff --git a/lib/screens/library_tabs/library_recommended_tab.dart b/lib/screens/library_tabs/library_recommended_tab.dart index 65b594d6..defdddea 100644 --- a/lib/screens/library_tabs/library_recommended_tab.dart +++ b/lib/screens/library_tabs/library_recommended_tab.dart @@ -26,6 +26,7 @@ class _LibraryRecommendedTabState } /// Focus the first item in the first hub + @override void focusFirstItem() { _hubNavigationController.focusHub(0, 0); } diff --git a/lib/screens/logs_screen.dart b/lib/screens/logs_screen.dart index 6b91e738..3be745ea 100644 --- a/lib/screens/logs_screen.dart +++ b/lib/screens/logs_screen.dart @@ -245,55 +245,16 @@ class _LogEntryCardState extends State<_LogEntryCard> { const SizedBox(height: 12), const Divider(), const SizedBox(height: 8), - if (widget.log.error != null) ...[ - Text( - t.logs.error, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: widget.levelColor, - fontWeight: FontWeight.bold, - ), + if (widget.log.error != null) + _buildDetailSection( + title: t.logs.error, + content: widget.log.error.toString(), ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[900] - : Colors.grey[200], - borderRadius: BorderRadius.circular(4), - ), - child: SelectableText( - widget.log.error.toString(), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - ), - ), - ], if (widget.log.stackTrace != null) ...[ const SizedBox(height: 12), - Text( - t.logs.stackTrace, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: widget.levelColor, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[900] - : Colors.grey[200], - borderRadius: BorderRadius.circular(4), - ), - child: SelectableText( - widget.log.stackTrace.toString(), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - ), + _buildDetailSection( + title: t.logs.stackTrace, + content: widget.log.stackTrace.toString(), ), ], ], @@ -303,4 +264,35 @@ class _LogEntryCardState extends State<_LogEntryCard> { ), ); } + + Widget _buildDetailSection({required String title, required String content}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: widget.levelColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey[900] + : Colors.grey[200], + borderRadius: BorderRadius.circular(4), + ), + child: SelectableText( + content, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), + ), + ), + ], + ); + } } diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index 97eac4df..557303c7 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -1,6 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../i18n/strings.g.dart'; diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 5d6562d2..4252127a 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:rate_limiter/rate_limiter.dart'; diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index 1da2db00..c1073a95 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../client/plex_client.dart'; import '../widgets/focus/focus_indicator.dart'; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e2f12b24..8b387805 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 7d39179e..315213c3 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -249,22 +249,53 @@ class VideoPlayerScreenState extends State player = Player(); // Configure player properties - await player!.setProperty('target-colorspace-hint', 'yes'); // Enable HDR passthrough + await player!.setProperty( + 'target-colorspace-hint', + 'yes', + ); // Enable HDR passthrough await player!.setProperty('sub-ass', 'yes'); // Enable libass await player!.setProperty('sub-fonts-dir', 'assets'); await player!.setProperty('sub-font', 'Go Noto Current-Regular'); - await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); - await player!.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error'); - await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); + await player!.setProperty( + 'demuxer-max-bytes', + bufferSizeBytes.toString(), + ); + await player!.setProperty( + 'msg-level', + debugLoggingEnabled ? 'all=debug' : 'all=error', + ); + await player!.setProperty( + 'hwdec', + _getHwdecValue(enableHardwareDecoding), + ); // Subtitle styling - await player!.setProperty('sub-font-size', settingsService.getSubtitleFontSize().toString()); - await player!.setProperty('sub-color', settingsService.getSubtitleTextColor()); - await player!.setProperty('sub-border-size', settingsService.getSubtitleBorderSize().toString()); - await player!.setProperty('sub-border-color', settingsService.getSubtitleBorderColor()); - final bgOpacity = (settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt(); - final bgColor = settingsService.getSubtitleBackgroundColor().replaceFirst('#', ''); - await player!.setProperty('sub-back-color', '#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor'); + await player!.setProperty( + 'sub-font-size', + settingsService.getSubtitleFontSize().toString(), + ); + await player!.setProperty( + 'sub-color', + settingsService.getSubtitleTextColor(), + ); + await player!.setProperty( + 'sub-border-size', + settingsService.getSubtitleBorderSize().toString(), + ); + await player!.setProperty( + 'sub-border-color', + settingsService.getSubtitleBorderColor(), + ); + final bgOpacity = + (settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt(); + final bgColor = settingsService.getSubtitleBackgroundColor().replaceFirst( + '#', + '', + ); + await player!.setProperty( + 'sub-back-color', + '#${bgOpacity.toRadixString(16).padLeft(2, '0').toUpperCase()}$bgColor', + ); await player!.setProperty('sub-ass-override', 'no'); // Platform-specific settings @@ -554,9 +585,7 @@ class VideoPlayerScreenState extends State final client = _getClientForMetadata(context); // Initialize playback service - final playbackService = PlaybackInitializationService( - client: client, - ); + final playbackService = PlaybackInitializationService(client: client); // Get playback data (video URL and available versions) final result = await playbackService.getPlaybackData( @@ -1146,44 +1175,44 @@ class VideoPlayerScreenState extends State children: [ // Video player Center( - child: LayoutBuilder( - builder: (context, constraints) { - // Update player size when layout changes - final newSize = Size( - constraints.maxWidth, - constraints.maxHeight, - ); + child: LayoutBuilder( + builder: (context, constraints) { + // Update player size when layout changes + final newSize = Size( + constraints.maxWidth, + constraints.maxHeight, + ); - // Update player size in video filter manager - if (_videoFilterManager != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _videoFilterManager!.updatePlayerSize(newSize); - } - }); - } + // Update player size in video filter manager + if (_videoFilterManager != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _videoFilterManager!.updatePlayerSize(newSize); + } + }); + } - return Video( - player: player!, - fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, - controls: (context) => plexVideoControlsBuilder( - player!, - widget.metadata, - onNext: _nextEpisode != null ? _playNext : null, - onPrevious: _previousEpisode != null - ? _playPrevious - : null, - availableVersions: _availableVersions, - selectedMediaIndex: widget.selectedMediaIndex, - boxFitMode: _videoFilterManager?.boxFitMode ?? 0, - onCycleBoxFitMode: _cycleBoxFitMode, - onAudioTrackChanged: _onAudioTrackChanged, - onSubtitleTrackChanged: _onSubtitleTrackChanged, - ), - ); - }, - ), + return Video( + player: player!, + fit: _videoFilterManager?.currentBoxFit ?? BoxFit.contain, + controls: (context) => plexVideoControlsBuilder( + player!, + widget.metadata, + onNext: _nextEpisode != null ? _playNext : null, + onPrevious: _previousEpisode != null + ? _playPrevious + : null, + availableVersions: _availableVersions, + selectedMediaIndex: widget.selectedMediaIndex, + boxFitMode: _videoFilterManager?.boxFitMode ?? 0, + onCycleBoxFitMode: _cycleBoxFitMode, + onAudioTrackChanged: _onAudioTrackChanged, + onSubtitleTrackChanged: _onSubtitleTrackChanged, + ), + ); + }, ), + ), // Play Next Dialog if (_showPlayNextDialog && _nextEpisode != null) Positioned.fill( diff --git a/lib/services/playback_initialization_service.dart b/lib/services/playback_initialization_service.dart index 15cc663e..8ab0d836 100644 --- a/lib/services/playback_initialization_service.dart +++ b/lib/services/playback_initialization_service.dart @@ -7,9 +7,7 @@ import '../i18n/strings.g.dart'; class PlaybackInitializationService { final PlexClient client; - PlaybackInitializationService({ - required this.client, - }); + PlaybackInitializationService({required this.client}); /// Fetch playback data for the given metadata /// diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 7abdaf28..8dd8191a 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -273,17 +273,7 @@ class StorageService { await _prefs.setString(_keyLibraryOrder, jsonString); } - List? getLibraryOrder() { - final jsonString = _prefs.getString(_keyLibraryOrder); - if (jsonString == null) return null; - - try { - final decoded = json.decode(jsonString) as List; - return decoded.map((e) => e.toString()).toList(); - } catch (e) { - return null; - } - } + List? getLibraryOrder() => _getStringList(_keyLibraryOrder); // User Profile (stored as JSON string) Future saveUserProfile(Map profileJson) async { @@ -406,8 +396,18 @@ class StorageService { await _prefs.setString(_keyServerOrder, jsonString); } - List? getServerOrder() { - final jsonString = _prefs.getString(_keyServerOrder); + List? getServerOrder() => _getStringList(_keyServerOrder); + + /// Clear server order + Future clearServerOrder() async { + await _prefs.remove(_keyServerOrder); + } + + // Private helper methods + + /// Helper to read and decode JSON `List` from preferences + List? _getStringList(String key) { + final jsonString = _prefs.getString(key); if (jsonString == null) return null; try { @@ -418,13 +418,6 @@ class StorageService { } } - /// Clear server order - Future clearServerOrder() async { - await _prefs.remove(_keyServerOrder); - } - - // Private helper methods - /// Helper to read and decode JSON Map from preferences /// /// [key] - The preference key to read diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index b2e2b93b..33813c25 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -139,21 +139,21 @@ class UpdateService { return updateInfo; } + /// Parse version string into list of integers + /// Handles versions like "1.2.3+4" by taking only the numeric parts + static List _parseVersionParts(String version) { + return version.split('.').map((p) { + final numPart = p.split('+').first.split('-').first; + return int.tryParse(numPart) ?? 0; + }).toList(); + } + /// Compare two version strings /// Returns true if newVersion is newer than currentVersion static bool _isNewerVersion(String newVersion, String currentVersion) { try { - // Split by '.' and parse as integers - final newParts = newVersion.split('.').map((p) { - // Handle versions like "1.2.3+4" by taking only the numeric part - final numPart = p.split('+').first.split('-').first; - return int.tryParse(numPart) ?? 0; - }).toList(); - - final currentParts = currentVersion.split('.').map((p) { - final numPart = p.split('+').first.split('-').first; - return int.tryParse(numPart) ?? 0; - }).toList(); + final newParts = _parseVersionParts(newVersion); + final currentParts = _parseVersionParts(currentVersion); // Compare each part final maxLength = newParts.length > currentParts.length diff --git a/lib/services/video_filter_manager.dart b/lib/services/video_filter_manager.dart index 85a728e9..a0b0500c 100644 --- a/lib/services/video_filter_manager.dart +++ b/lib/services/video_filter_manager.dart @@ -240,9 +240,7 @@ class VideoFilterManager { 'Clearing video filter - aspect ratios similar, applying base margins (player: $_playerSize, video: $_videoSize)', ); await player.setProperty('vf', ''); - await player.setProperty('sub-margin-x', '20'); // Base margin - await player.setProperty('sub-margin-y', '40'); // Base margin - await player.setProperty('sub-scale', '1.0'); // Reset scale + await _applyBaseSubtitleMargins(); } } else if (_boxFitMode == 2) { // Stretch/fill mode - override aspect ratio to match player @@ -254,22 +252,24 @@ class VideoFilterManager { // Override video aspect ratio to match player aspect ratio (stretches video) if (_playerSize != null) { final playerAspect = _playerSize!.width / _playerSize!.height; - await player.setProperty('video-aspect-override', playerAspect.toString()); + await player.setProperty( + 'video-aspect-override', + playerAspect.toString(), + ); } - await player.setProperty('sub-margin-x', '20'); // Base margin - await player.setProperty('sub-margin-y', '40'); // Base margin - await player.setProperty('sub-scale', '1.0'); // Reset scale + await _applyBaseSubtitleMargins(); } else { // Contain mode (0) - clear video filter and reset aspect ratio appLogger.d( 'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode', ); await player.setProperty('vf', ''); - await player.setProperty('video-aspect-override', 'no'); // Reset to original aspect - await player.setProperty('sub-margin-x', '20'); // Base margin - await player.setProperty('sub-margin-y', '40'); // Base margin - await player.setProperty('sub-scale', '1.0'); // Reset scale + await player.setProperty( + 'video-aspect-override', + 'no', + ); // Reset to original aspect + await _applyBaseSubtitleMargins(); } } catch (e) { appLogger.w('Failed to update video filter', error: e); @@ -281,6 +281,13 @@ class VideoFilterManager { /// subsequent calls within 50ms are debounced. void debouncedUpdateVideoFilter() => _debouncedUpdateVideoFilter(); + /// Apply base subtitle margins (used when no custom crop margins needed) + Future _applyBaseSubtitleMargins() async { + await player.setProperty('sub-margin-x', '20'); + await player.setProperty('sub-margin-y', '40'); + await player.setProperty('sub-scale', '1.0'); + } + /// Clean up resources void dispose() { _debouncedUpdateVideoFilter.cancel(); diff --git a/lib/widgets/filters_bottom_sheet.dart b/lib/widgets/filters_bottom_sheet.dart index 10d8fd06..e66e6346 100644 --- a/lib/widgets/filters_bottom_sheet.dart +++ b/lib/widgets/filters_bottom_sheet.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import '../models/plex_filter.dart'; import '../widgets/app_bar_back_button.dart'; import '../widgets/bottom_sheet_header.dart'; diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 5d36864e..892fe333 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -1543,7 +1543,7 @@ class _FocusableContextMenuSheetState selected: isFocused, selectedTileColor: Theme.of( context, - ).colorScheme.primary.withOpacity(0.1), + ).colorScheme.primary.withValues(alpha: 0.1), ); }, ), diff --git a/lib/widgets/video_controls/mobile_video_controls.dart b/lib/widgets/video_controls/mobile_video_controls.dart index b5c9cc7c..56a2cbc8 100644 --- a/lib/widgets/video_controls/mobile_video_controls.dart +++ b/lib/widgets/video_controls/mobile_video_controls.dart @@ -121,85 +121,43 @@ class MobileVideoControls extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - child: Semantics( - label: t.videoControls.seekBackwardButton( - seconds: seekTimeSmall, - ), - button: true, - excludeSemantics: true, - child: IconButton( - icon: Icon( - getReplayIcon(seekTimeSmall), - color: Colors.white, - size: 48, - ), - iconSize: 48, - onPressed: () { - seekWithClamping(player, Duration(seconds: -seekTimeSmall)); - }, - ), + _buildCircularButton( + semanticLabel: t.videoControls.seekBackwardButton( + seconds: seekTimeSmall, ), + icon: getReplayIcon(seekTimeSmall), + iconSize: 48, + onPressed: () { + seekWithClamping(player, Duration(seconds: -seekTimeSmall)); + }, ), const SizedBox(width: 48), - Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - child: Semantics( - label: isPlaying - ? t.videoControls.pauseButton - : t.videoControls.playButton, - button: true, - excludeSemantics: true, - child: IconButton( - icon: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 72, - ), - iconSize: 72, - onPressed: () { - if (isPlaying) { - player.pause(); - onCancelAutoHide?.call(); // Cancel auto-hide when paused - } else { - player.play(); - onStartAutoHide?.call(); // Start auto-hide when playing - } - }, - ), - ), + _buildCircularButton( + semanticLabel: isPlaying + ? t.videoControls.pauseButton + : t.videoControls.playButton, + icon: isPlaying ? Icons.pause : Icons.play_arrow, + iconSize: 72, + onPressed: () { + if (isPlaying) { + player.pause(); + onCancelAutoHide?.call(); // Cancel auto-hide when paused + } else { + player.play(); + onStartAutoHide?.call(); // Start auto-hide when playing + } + }, ), const SizedBox(width: 48), - Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - child: Semantics( - label: t.videoControls.seekForwardButton( - seconds: seekTimeSmall, - ), - button: true, - excludeSemantics: true, - child: IconButton( - icon: Icon( - getForwardIcon(seekTimeSmall), - color: Colors.white, - size: 48, - ), - iconSize: 48, - onPressed: () { - seekWithClamping(player, Duration(seconds: seekTimeSmall)); - }, - ), + _buildCircularButton( + semanticLabel: t.videoControls.seekForwardButton( + seconds: seekTimeSmall, ), + icon: getForwardIcon(seekTimeSmall), + iconSize: 48, + onPressed: () { + seekWithClamping(player, Duration(seconds: seekTimeSmall)); + }, ), ], ); @@ -266,6 +224,30 @@ class MobileVideoControls extends StatelessWidget { ); } + Widget _buildCircularButton({ + required String semanticLabel, + required IconData icon, + required double iconSize, + required VoidCallback onPressed, + }) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + child: Semantics( + label: semanticLabel, + button: true, + excludeSemantics: true, + child: IconButton( + icon: Icon(icon, color: Colors.white, size: iconSize), + iconSize: iconSize, + onPressed: onPressed, + ), + ), + ); + } + /// Conditionally wraps child with SafeArea only in portrait mode Widget _conditionalSafeArea({ required BuildContext context, diff --git a/lib/widgets/video_controls/sheets/track_selection_sheet.dart b/lib/widgets/video_controls/sheets/track_selection_sheet.dart index 291f82c9..17989b14 100644 --- a/lib/widgets/video_controls/sheets/track_selection_sheet.dart +++ b/lib/widgets/video_controls/sheets/track_selection_sheet.dart @@ -115,7 +115,8 @@ class TrackSelectionSheet extends StatelessWidget { final selectedTrack = getCurrentTrack(currentTrack); // Determine if "Off" is selected (null or explicit off) - final isOffSelected = selectedTrack == null || + final isOffSelected = + selectedTrack == null || (isOffTrack?.call(selectedTrack) ?? false); final itemCount = diff --git a/lib/widgets/video_controls/video_controls.dart b/lib/widgets/video_controls/video_controls.dart index e4db8dbb..cfda4566 100644 --- a/lib/widgets/video_controls/video_controls.dart +++ b/lib/widgets/video_controls/video_controls.dart @@ -202,7 +202,9 @@ class _PlexVideoControlsState extends State /// Listen to completed stream to show controls when video ends void _listenToCompleted() { - _completedSubscription = widget.player.streams.completed.listen((completed) { + _completedSubscription = widget.player.streams.completed.listen(( + completed, + ) { if (completed && mounted) { // Show controls when video completes (for play next dialog etc.) setState(() { @@ -813,52 +815,53 @@ class _PlexVideoControlsState extends State stops: const [0.0, 0.2, 0.8, 1.0], ), ), - child: isMobile - ? Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (_) => - _restartHideTimerIfPlaying(), - child: MobileVideoControls( - player: widget.player, - metadata: widget.metadata, - chapters: _chapters, - chaptersLoaded: _chaptersLoaded, - seekTimeSmall: _seekTimeSmall, - trackChapterControls: - _buildTrackChapterControlsWidget(), - onSeek: _throttledSeek, - onSeekEnd: _finalizeSeek, - onPlayPause: - () {}, // Not used, handled internally - onCancelAutoHide: () => _hideTimer?.cancel(), - onStartAutoHide: _startHideTimer, - ), - ) - : Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (_) => - _restartHideTimerIfPlaying(), - child: DesktopVideoControls( - player: widget.player, - metadata: widget.metadata, - onNext: widget.onNext, - onPrevious: widget.onPrevious, - chapters: _chapters, - chaptersLoaded: _chaptersLoaded, - seekTimeSmall: _seekTimeSmall, - volumeControl: VolumeControl( + child: isMobile + ? Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (_) => + _restartHideTimerIfPlaying(), + child: MobileVideoControls( player: widget.player, + metadata: widget.metadata, + chapters: _chapters, + chaptersLoaded: _chaptersLoaded, + seekTimeSmall: _seekTimeSmall, + trackChapterControls: + _buildTrackChapterControlsWidget(), + onSeek: _throttledSeek, + onSeekEnd: _finalizeSeek, + onPlayPause: + () {}, // Not used, handled internally + onCancelAutoHide: () => _hideTimer?.cancel(), + onStartAutoHide: _startHideTimer, + ), + ) + : Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (_) => + _restartHideTimerIfPlaying(), + child: DesktopVideoControls( + player: widget.player, + metadata: widget.metadata, + onNext: widget.onNext, + onPrevious: widget.onPrevious, + chapters: _chapters, + chaptersLoaded: _chaptersLoaded, + seekTimeSmall: _seekTimeSmall, + volumeControl: VolumeControl( + player: widget.player, + ), + trackChapterControls: + _buildTrackChapterControlsWidget(), + onSeekToPreviousChapter: + _seekToPreviousChapter, + onSeekToNextChapter: _seekToNextChapter, + onSeek: _throttledSeek, + onSeekEnd: _finalizeSeek, + getReplayIcon: getReplayIcon, + getForwardIcon: getForwardIcon, ), - trackChapterControls: - _buildTrackChapterControlsWidget(), - onSeekToPreviousChapter: _seekToPreviousChapter, - onSeekToNextChapter: _seekToNextChapter, - onSeek: _throttledSeek, - onSeekEnd: _finalizeSeek, - getReplayIcon: getReplayIcon, - getForwardIcon: getForwardIcon, ), - ), ), ), ),