From 9bd5732f2bd9785ecb1f090f41c3cb91e88ac0b3 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 1 May 2026 05:02:26 +0200 Subject: [PATCH] chore: clean up redundant comments --- lib/database/app_database.dart | 12 --- lib/database/download_operations.dart | 18 ----- lib/main.dart | 14 ---- lib/models/external_player_models.dart | 4 - lib/mpv/player/platform/player_android.dart | 44 ----------- lib/mpv/player/player.dart | 44 ----------- lib/mpv/player/player_base.dart | 20 ----- lib/mpv/player/player_native.dart | 32 -------- lib/providers/download_provider.dart | 4 - lib/screens/livetv/live_tv_screen.dart | 8 -- lib/screens/livetv/tabs/guide_tab.dart | 28 ------- lib/screens/livetv/tabs/whats_on_tab.dart | 6 -- lib/screens/plex_metadata_edit_screen.dart | 4 - lib/screens/profile/pin_entry_dialog.dart | 12 --- .../settings/appearance_settings_screen.dart | 15 ---- .../settings/playback_settings_screen.dart | 15 ---- lib/screens/settings/settings_screen.dart | 14 ---- .../settings/subtitle_styling_screen.dart | 3 - lib/screens/video_player_screen.dart | 79 ++----------------- lib/services/download_manager_service.dart | 61 +------------- lib/services/download_storage_service.dart | 38 --------- lib/services/plex_client.dart | 48 ----------- lib/services/settings_export_service.dart | 2 - lib/services/track_selection_service.dart | 3 - lib/utils/media_server_http_client.dart | 24 ------ 25 files changed, 8 insertions(+), 544 deletions(-) diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index 6623369d..34321d89 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -215,10 +215,6 @@ class AppDatabase extends _$AppDatabase { } } - // ============================================================ - // Offline Watch Progress Operations - // ============================================================ - Expression _clientScopePredicate(GeneratedColumn column, String? clientScopeId) { return clientScopeId == null ? column.isNull() : column.equals(clientScopeId); } @@ -446,10 +442,6 @@ class AppDatabase extends _$AppDatabase { return delete(offlineWatchProgress).go(); } - // ============================================================ - // Sync Rules Operations - // ============================================================ - Future> getSyncRules({String? profileId}) { final query = select(syncRules); if (profileId != null) { @@ -550,10 +542,6 @@ class AppDatabase extends _$AppDatabase { await (delete(syncRules)..where((t) => t.globalKey.equals(globalKey))).go(); } - // ============================================================ - // Downloaded Media Queries for Watch State Sync - // ============================================================ - /// Get all downloaded media items (for syncing watch states) Future> getAllDownloadedMetadata() { return (select(downloadedMedia)..where((t) => t.status.equals(DownloadStatus.completed.index))).get(); diff --git a/lib/database/download_operations.dart b/lib/database/download_operations.dart index 90f49591..53a40d93 100644 --- a/lib/database/download_operations.dart +++ b/lib/database/download_operations.dart @@ -4,7 +4,6 @@ import 'app_database.dart'; import '../models/download_models.dart'; import '../profiles/profile.dart'; -/// Extension methods on AppDatabase for download operations extension DownloadDatabaseOperations on AppDatabase { Future addDownloadOwner({required String profileId, required String globalKey}) async { if (profileId.isEmpty) return; @@ -76,7 +75,6 @@ extension DownloadDatabaseOperations on AppDatabase { } } - /// Insert a new download into the database. Future insertDownload({ required String serverId, String? clientScopeId, @@ -104,7 +102,6 @@ extension DownloadDatabaseOperations on AppDatabase { ); } - /// Add item to download queue Future addToQueue({ required String mediaGlobalKey, int priority = 0, @@ -143,14 +140,12 @@ extension DownloadDatabaseOperations on AppDatabase { return result?.readTable(downloadQueue); } - /// Update download status Future updateDownloadStatus(String globalKey, int status) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(status: Value(status))); } - /// Update download progress Future updateDownloadProgress(String globalKey, int progress, int downloadedBytes, int totalBytes) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( DownloadedMediaCompanion( @@ -161,7 +156,6 @@ extension DownloadDatabaseOperations on AppDatabase { ); } - /// Update video file path Future updateVideoFilePath(String globalKey, String filePath) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( DownloadedMediaCompanion( @@ -171,16 +165,13 @@ extension DownloadDatabaseOperations on AppDatabase { ); } - /// Update artwork paths Future updateArtworkPaths({required String globalKey, String? thumbPath}) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(thumbPath: Value(thumbPath))); } - /// Update download error and increment retry count Future updateDownloadError(String globalKey, String errorMessage) async { - // Get current retry count to increment it final existing = await getDownloadedMedia(globalKey); final currentCount = existing?.retryCount ?? 0; @@ -189,31 +180,26 @@ extension DownloadDatabaseOperations on AppDatabase { ); } - /// Clear download error and reset retry count (for retry) Future clearDownloadError(String globalKey) async { await (update(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).write( const DownloadedMediaCompanion(errorMessage: Value(null), retryCount: Value(0)), ); } - /// Remove item from queue Future removeFromQueue(String mediaGlobalKey) async { await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(mediaGlobalKey))).go(); } - /// Get downloaded media item Future getDownloadedMedia(String globalKey) { return (select(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).getSingleOrNull(); } - /// Delete a download Future deleteDownload(String globalKey) async { await (delete(downloadOwners)..where((t) => t.globalKey.equals(globalKey))).go(); await (delete(downloadedMedia)..where((t) => t.globalKey.equals(globalKey))).go(); await (delete(downloadQueue)..where((t) => t.mediaGlobalKey.equals(globalKey))).go(); } - /// Get all downloaded episodes for a season Future> getEpisodesBySeason( String seasonKey, { String? serverId, @@ -229,7 +215,6 @@ extension DownloadDatabaseOperations on AppDatabase { .get(); } - /// Get all downloaded episodes for a show Future> getEpisodesByShow( String showKey, { String? serverId, @@ -245,7 +230,6 @@ extension DownloadDatabaseOperations on AppDatabase { .get(); } - /// Get all downloaded items for a specific server Future> getDownloadsByServerId(String serverId) { return (select(downloadedMedia)..where((t) => t.serverId.equals(serverId))).get(); } @@ -268,14 +252,12 @@ extension DownloadDatabaseOperations on AppDatabase { return column.equals(clientScopeId); } - /// Update the background_downloader task ID for a download Future updateBgTaskId(String globalKey, String? taskId) async { await (update( downloadedMedia, )..where((t) => t.globalKey.equals(globalKey))).write(DownloadedMediaCompanion(bgTaskId: Value(taskId))); } - /// Get the background_downloader task ID for a download Future getBgTaskId(String globalKey) async { final item = await getDownloadedMedia(globalKey); return item?.bgTaskId; diff --git a/lib/main.dart b/lib/main.dart index b2d11679..557d3263 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -146,11 +146,9 @@ Future main() async { } Future _bootstrapApp() async { - // Initialize settings first to get saved locale final settings = await SettingsService.getInstance(); final savedLocale = settings.read(SettingsService.appLocale); - // Initialize localization with saved locale unawaited(LocaleSettings.setLocale(savedLocale)); // Needed for formatting dates in different locales @@ -165,10 +163,8 @@ Future _bootstrapApp() async { PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; // 100MB } - // Initialize services in parallel where possible final futures = >[]; - // Initialize window_manager for desktop platforms if (PlatformDetector.isDesktopOS()) { futures.add(windowManager.ensureInitialized()); } @@ -178,7 +174,6 @@ Future _bootstrapApp() async { futures.add(TvDetectionService.getInstance(forceTv: settings.read(SettingsService.forceTvMode))); } if (Platform.isAndroid) { - // Initialize PiP service to listen for PiP state changes (Android only). PipService(); } @@ -188,21 +183,17 @@ Future _bootstrapApp() async { // Hook Windows native fullscreen callback (no-op elsewhere). NativeWindowService.initialize(); - // Initialize storage service futures.add(StorageService.getInstance()); - // Wait for all parallel services to complete await Future.wait(futures); // The PLEX_TOKEN dart-define (screenshot automation) is consumed by // [ConnectionBootstrap.seedFromDevTokenDefine] later, when the registry // is available — keeps the deprecated legacy slots out of runtime paths. - // Initialize logger level based on debug setting final debugEnabled = settings.read(SettingsService.enableDebugLogging); setLoggerLevel(debugEnabled); - // Log app version and git commit at startup final packageInfo = await PackageInfo.fromPlatform(); final commitSuffix = gitCommit.isNotEmpty ? ' (${gitCommit.substring(0, 7)})' : ''; String renderer = ''; @@ -211,10 +202,8 @@ Future _bootstrapApp() async { } appLogger.i('Plezy v${packageInfo.version}+${packageInfo.buildNumber}$commitSuffix$renderer'); - // Initialize download storage service with settings await DownloadStorageService.instance.initialize(settings); - // Start global fullscreen state monitoring FullscreenStateManager().startMonitoring(); // Apply "start in fullscreen" preference on Windows/Linux. macOS is @@ -236,8 +225,6 @@ Future _bootstrapApp() async { // Trakt scrobble service (all platforms) await TraktScrobbleService.instance.initialize(); - // DTD service is available for MCP tooling connection if needed - // Register bundled shader licenses _registerShaderLicenses(); @@ -788,7 +775,6 @@ class _MainAppState extends State with WidgetsBindingObserver { return previous ?? OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider); }, ), - // Existing providers ChangeNotifierProxyProvider2( create: (_) => UserProfileProvider(), update: (context, activeProfile, connections, previous) { diff --git a/lib/models/external_player_models.dart b/lib/models/external_player_models.dart index c2ae6a9f..d8b6ae81 100644 --- a/lib/models/external_player_models.dart +++ b/lib/models/external_player_models.dart @@ -79,8 +79,6 @@ class ExternalPlayer { int get hashCode => id.hashCode; } -// --- Launch helpers --- - Future _launchWithUrl(String url) { return launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } @@ -146,8 +144,6 @@ Future _launchCustom(String value, String url, CustomPlayerType type) asyn } } -// --- Known Players --- - class KnownPlayers { static final systemDefault = ExternalPlayer(id: 'system_default', name: 'System Default', launch: _launchWithUrl); diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index 9aeb8fff..5b80186d 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -30,10 +30,6 @@ class PlayerAndroid extends PlayerBase { @override bool get supportsSecondarySubtitles => false; - // ============================================ - // Platform-Specific Event Handling - // ============================================ - @override void handlePlayerEvent(String name, Map? data) { // Handle Android-specific events @@ -50,10 +46,6 @@ class PlayerAndroid extends PlayerBase { super.handlePlayerEvent(name, data); } - // ============================================ - // Initialization - // ============================================ - // Memoizes the in-flight init Future so concurrent callers share one // `invoke('initialize')`. ExoPlayer's native handleInitialize is // synchronous and would mask a Dart-side race anyway, but we mirror the @@ -100,10 +92,6 @@ class PlayerAndroid extends PlayerBase { } } - // ============================================ - // Playback Control - // ============================================ - @override Future open( Media media, { @@ -154,10 +142,6 @@ class PlayerAndroid extends PlayerBase { await runSeek(() => invoke('seek', {'positionMs': position.inMilliseconds})); } - // ============================================ - // Track Selection - // ============================================ - @override Future selectAudioTrack(AudioTrack track) async { await invoke('selectAudioTrack', {'trackId': track.id}); @@ -173,10 +157,6 @@ class PlayerAndroid extends PlayerBase { await invoke('addSubtitleTrack', {'uri': uri, 'title': title, 'language': language, 'select': select}); } - // ============================================ - // Volume and Rate - // ============================================ - @override Future setVolume(double volume) async { await invoke('setVolume', {'volume': volume}); @@ -187,10 +167,6 @@ class PlayerAndroid extends PlayerBase { await invoke('setRate', {'rate': rate}); } - // ============================================ - // MPV Properties (Compatibility Layer) - // ============================================ - @override Future setProperty(String name, String value) async { if (disposed) return; @@ -352,10 +328,6 @@ class PlayerAndroid extends PlayerBase { } } - // ============================================ - // Subtitle Styling (ExoPlayer Native) - // ============================================ - /// Apply subtitle styling to the native ExoPlayer layer. /// /// For non-ASS subtitles, applies CaptionStyleCompat (color, border, background). @@ -385,10 +357,6 @@ class PlayerAndroid extends PlayerBase { }); } - // ============================================ - // Box-fit / video scaling mode - // ============================================ - /// Apply the box-fit mode to the native ExoPlayer layer. /// Maps to AspectRatioFrameLayout resize mode: 0=FIT, 1=ZOOM, 2=FILL. Future setBoxFitMode(int mode) async { @@ -396,10 +364,6 @@ class PlayerAndroid extends PlayerBase { await invoke('setBoxFitMode', {'mode': mode}); } - // ============================================ - // Frame Rate Matching - // ============================================ - @override Future setVideoFrameRate(double fps, int durationMs, {int extraDelayMs = 0}) async { if (disposed || !initialized) return false; @@ -423,10 +387,6 @@ class PlayerAndroid extends PlayerBase { await invoke('updateFrame'); } - // ============================================ - // Audio Focus - // ============================================ - @override Future requestAudioFocus() async { if (disposed) return false; @@ -440,10 +400,6 @@ class PlayerAndroid extends PlayerBase { await invoke('abandonAudioFocus'); } - // ============================================ - // Log Level - // ============================================ - @override Future setLogLevel(String level) async { if (disposed) return; diff --git a/lib/mpv/player/player.dart b/lib/mpv/player/player.dart index b827ed07..18360a06 100644 --- a/lib/mpv/player/player.dart +++ b/lib/mpv/player/player.dart @@ -53,10 +53,6 @@ abstract class Player { /// The type of player backend being used (e.g., 'mpv', 'exoplayer'). String get playerType; - // ============================================ - // Playback Control - // ============================================ - /// Open a media source for playback. /// /// [media] - The media source to open. @@ -78,10 +74,6 @@ abstract class Player { /// Seek to a specific position. Future seek(Duration position); - // ============================================ - // Track Selection - // ============================================ - /// Select an audio track. Future selectAudioTrack(AudioTrack track); @@ -107,10 +99,6 @@ abstract class Player { /// [select] - Whether to select this track immediately. Future addSubtitleTrack({required String uri, String? title, String? language, bool select = false}); - // ============================================ - // Volume and Rate - // ============================================ - /// Set the playback volume. /// /// [volume] - Volume level from 0.0 (muted) to 100.0 (max). @@ -126,10 +114,6 @@ abstract class Player { /// [device] - The audio device to use. Future setAudioDevice(AudioDevice device); - // ============================================ - // MPV Properties (Advanced) - // ============================================ - /// Set an MPV property by name. /// /// Common properties: @@ -162,30 +146,18 @@ abstract class Player { /// [args] - Command and arguments as a list of strings. Future command(List args); - // ============================================ - // Subtitle Fonts - // ============================================ - /// Configure subtitle fonts for libass rendering. /// /// Extracts a comprehensive Unicode font (Go Noto) to the cache directory /// and sets `sub-fonts-dir` and `sub-font` properties. Future configureSubtitleFonts(); - // ============================================ - // Passthrough Mode (Audio) - // ============================================ - /// Enable or disable audio passthrough mode. /// /// When enabled, supported audio codecs (AC3, DTS, etc.) will be /// passed through to the audio device without decoding. Future setAudioPassthrough(bool enabled); - // ============================================ - // Visibility (macOS Metal Layer) - // ============================================ - /// Show or hide the video rendering layer. /// /// On macOS, this controls the Metal layer visibility. @@ -201,10 +173,6 @@ abstract class Player { /// On other platforms, this is a no-op. Future updateFrame(); - // ============================================ - // Frame Rate Matching (Android) - // ============================================ - /// Set the video frame rate for display refresh rate matching. /// /// On Android, this hints the system to adjust the display refresh rate @@ -231,10 +199,6 @@ abstract class Player { /// On other platforms, this is a no-op. Future clearVideoFrameRate(); - // ============================================ - // Audio Focus (Android) - // ============================================ - /// Request audio focus before starting playback. /// /// On Android, this notifies the system that the app wants to play audio, @@ -252,10 +216,6 @@ abstract class Player { /// On other platforms, this is a no-op. Future abandonAudioFocus(); - // ============================================ - // Lifecycle - // ============================================ - /// Whether the player has been disposed. bool get disposed; @@ -264,10 +224,6 @@ abstract class Player { /// After calling this, the player instance should not be used. Future dispose(); - // ============================================ - // Factory - // ============================================ - /// Creates a new player instance. /// /// Returns a platform-specific implementation: diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index 588bbf36..742e67fc 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -510,10 +510,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { return methodChannel.invokeMethod(method, args); } - // ============================================ - // Default Implementations - // ============================================ - @override Future playOrPause() async { if (_disposed) return; @@ -576,10 +572,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { // ignore: no-empty-block - base no-op, overridden by platform subclasses Future setLogLevel(String level) async {} - // ============================================ - // Subtitle Fonts - // ============================================ - @override Future configureSubtitleFonts() async { try { @@ -596,10 +588,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } } - // ============================================ - // Seek helpers - // ============================================ - /// Run a backend-specific seek call, swallowing the common "not ready" errors /// the native channel throws when the engine was torn down mid-seek. @protected @@ -615,10 +603,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } } - // ============================================ - // Debug helpers - // ============================================ - /// Injects the log + error events that would fire when the server rejects the /// stream with HTTP 500 (shared-user bandwidth / transcoding limit). Used by /// the in-player debug button to preview the end-to-end detection path @@ -635,10 +619,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { errorController.add(const PlayerError('HTTP 500', cause: PlayerError.serverHttp500)); } - // ============================================ - // Lifecycle - // ============================================ - @override Future dispose() async { if (_disposed) return; diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 4b10f97c..63dd6131 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -33,10 +33,6 @@ class PlayerNative extends PlayerBase { /// but as JSON strings on Android/Windows. static final String _nodeFormat = (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node'; - // ============================================ - // Initialization - // ============================================ - // Memoizes the in-flight init Future so concurrent callers (e.g. the // parallel `requestAudioFocus()` and `setProperty()` paths kicked off in // VideoPlayerScreen._initializePlayer) share one `invoke('initialize')`. @@ -91,10 +87,6 @@ class PlayerNative extends PlayerBase { } } - // ============================================ - // Playback Control - // ============================================ - /// Opens a content:// URI via the platform channel and returns the raw FD number. /// Returns null if the call fails. Future _openContentFd(String contentUri) async { @@ -174,10 +166,6 @@ class PlayerNative extends PlayerBase { await runSeek(() => command(['seek', (position.inMilliseconds / 1000.0).toString(), 'absolute'])); } - // ============================================ - // Track Selection - // ============================================ - @override Future selectAudioTrack(AudioTrack track) async { await setProperty('aid', track.id); @@ -201,10 +189,6 @@ class PlayerNative extends PlayerBase { await command(args); } - // ============================================ - // Volume and Rate - // ============================================ - @override Future setVolume(double volume) async { await setProperty('volume', volume.toString()); @@ -220,10 +204,6 @@ class PlayerNative extends PlayerBase { await setProperty('audio-device', device.name); } - // ============================================ - // MPV Properties - // ============================================ - @override Future setProperty(String name, String value) async { if (disposed) return; @@ -245,10 +225,6 @@ class PlayerNative extends PlayerBase { await invoke('command', {'args': args}); } - // ============================================ - // Log Level - // ============================================ - @override Future setLogLevel(String level) async { if (disposed) return; @@ -256,10 +232,6 @@ class PlayerNative extends PlayerBase { await invoke('setLogLevel', {'level': level}); } - // ============================================ - // Passthrough - // ============================================ - @override Future setAudioPassthrough(bool enabled) async { if (enabled) { @@ -271,10 +243,6 @@ class PlayerNative extends PlayerBase { } } - // ============================================ - // Platform-Specific Overrides - // ============================================ - @override Future updateFrame() async { if (disposed || !initialized) return; diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index 12ee513e..2ff6e2bf 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -1488,10 +1488,6 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin return deletedTitles; } - // ============================================================ - // Sync Rules - // ============================================================ - /// All sync rules for the active profile (profile-scoped globalKey -> SyncRuleItem). Map get syncRules => Map.unmodifiable(_syncRules); diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index 1dbe693b..5c3e45eb 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -387,10 +387,6 @@ class _LiveTvScreenState extends State @override void focusActiveTabIfReady() => _focusCurrentTab(); - // --------------------------------------------------------------------------- - // Tab chips - // --------------------------------------------------------------------------- - String _getTabLabel(LiveTvTab tab) { return switch (tab) { LiveTvTab.guide => t.liveTv.guide, @@ -398,10 +394,6 @@ class _LiveTvScreenState extends State }; } - // --------------------------------------------------------------------------- - // Build - // --------------------------------------------------------------------------- - List _buildTabChipItems() { return [ for (int i = 0; i < LiveTvTab.values.length; i++) ...[ diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 36794f5b..9f9c281b 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -267,10 +267,6 @@ class GuideTabState extends State { await tuneAndNavigateToLiveTv(context, multiServer: multiServer, channel: channel, channels: widget.channels); } - // --------------------------------------------------------------------------- - // Focus key handling - // --------------------------------------------------------------------------- - KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) { final key = event.logicalKey; @@ -415,10 +411,6 @@ class GuideTabState extends State { return KeyEventResult.ignored; } - // --------------------------------------------------------------------------- - // Focus helpers - // --------------------------------------------------------------------------- - LiveTvProgram? _findCurrentProgram(int channelIndex) { if (channelIndex < 0 || channelIndex >= widget.channels.length) return null; final channel = widget.channels[channelIndex]; @@ -502,10 +494,6 @@ class GuideTabState extends State { } } - // --------------------------------------------------------------------------- - // Build - // --------------------------------------------------------------------------- - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -760,10 +748,6 @@ class GuideTabState extends State { }); } - // --------------------------------------------------------------------------- - // Time navigation bar - // --------------------------------------------------------------------------- - Widget _timeNavFocusWrap({required Widget child, required int index, required ThemeData theme}) { final isFocused = _hasFocus && _focusZone == _GuideZone.timeNav && _timeNavIndex == index; if (!isFocused) return child; @@ -840,10 +824,6 @@ class GuideTabState extends State { ); } - // --------------------------------------------------------------------------- - // Time header & now indicator - // --------------------------------------------------------------------------- - Widget _buildTimeHeader(ThemeData theme) { final is24Hour = MediaQuery.alwaysUse24HourFormatOf(context); final slots = []; @@ -872,10 +852,6 @@ class GuideTabState extends State { return Row(children: slots); } - // --------------------------------------------------------------------------- - // Channel column - // --------------------------------------------------------------------------- - Widget _buildChannelCell(LiveTvChannel channel, ThemeData theme, {required int index}) { final multiServer = context.read(); final client = multiServer.getClientForServer(channel.serverId ?? ''); @@ -918,10 +894,6 @@ class GuideTabState extends State { ); } - // --------------------------------------------------------------------------- - // Program grid - // --------------------------------------------------------------------------- - Widget _buildProgramRow( LiveTvChannel channel, List programs, diff --git a/lib/screens/livetv/tabs/whats_on_tab.dart b/lib/screens/livetv/tabs/whats_on_tab.dart index 4b6d3f3e..cab0ecd4 100644 --- a/lib/screens/livetv/tabs/whats_on_tab.dart +++ b/lib/screens/livetv/tabs/whats_on_tab.dart @@ -200,10 +200,8 @@ class WhatsOnTabState extends State with LiveTvActionsMixin { } } -// --------------------------------------------------------------------------- -// Poster card — always 2:3, shows poster image + title + subtitle -// --------------------------------------------------------------------------- - class _LiveTvPosterCard extends StatelessWidget { final LiveTvHubEntry entry; final double width; diff --git a/lib/screens/plex_metadata_edit_screen.dart b/lib/screens/plex_metadata_edit_screen.dart index 1218b648..e23d042b 100644 --- a/lib/screens/plex_metadata_edit_screen.dart +++ b/lib/screens/plex_metadata_edit_screen.dart @@ -396,8 +396,6 @@ class _PlexMetadataEditScreenState extends State { return options.first.label; } - // ===== Field visibility ===== - bool get _showSortTitle => _mediaType != MediaKind.season; bool get _showOriginalTitle => _mediaType == MediaKind.movie || _mediaType == MediaKind.show; bool get _showReleaseDate => _mediaType != MediaKind.season; @@ -840,8 +838,6 @@ class _PlexMetadataEditScreenState extends State { } } -// ===== Language option lists ===== - // Plex locale codes for metadata agent language. const _plexLocaleCodes = [ 'ar-SA', diff --git a/lib/screens/profile/pin_entry_dialog.dart b/lib/screens/profile/pin_entry_dialog.dart index b9dc1628..b646344f 100644 --- a/lib/screens/profile/pin_entry_dialog.dart +++ b/lib/screens/profile/pin_entry_dialog.dart @@ -118,10 +118,6 @@ class _PinEntryDialogState extends State with SingleTickerProvid } } -// --------------------------------------------------------------------------- -// _TvPinInput — unified 4-digit PIN input -// --------------------------------------------------------------------------- - class _TvPinInput extends StatefulWidget { final ValueChanged onSubmit; final VoidCallback onCancel; @@ -205,8 +201,6 @@ class _TvPinInputState extends State<_TvPinInput> with KeyRepeatHelper<_TvPinInp if (pin != null) widget.onSubmit(pin); } - // -- D-pad / keyboard handling (TV + desktop) -- - void _incrementDigit() { setState(() { _digits[_activeIndex] = ((_digits[_activeIndex] ?? -1) + 1) % 10; @@ -324,8 +318,6 @@ class _TvPinInputState extends State<_TvPinInput> with KeyRepeatHelper<_TvPinInp return KeyEventResult.ignored; } - // -- Mobile input handling -- - void _onMobileDigitChanged(int index, String value) { if (value.isEmpty) { // Backspace @@ -437,10 +429,6 @@ class _TvPinInputState extends State<_TvPinInput> with KeyRepeatHelper<_TvPinInp } } -// --------------------------------------------------------------------------- -// _DigitBox — single digit display box for TV/desktop -// --------------------------------------------------------------------------- - class _DigitBox extends StatelessWidget { final int? digit; final bool isActive; diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index fe840f70..4d3c513f 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -54,7 +54,6 @@ class _AppearanceSettingsScreenState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - // --- Display --- SettingsSectionHeader(t.settings.display), _buildThemeSelector(), _buildLanguageSelector(), @@ -64,13 +63,11 @@ class _AppearanceSettingsScreenState extends State { _buildShowEpisodeNumberOnCards(), _buildShowSeasonPostersOnTabs(), - // --- Home Screen --- SettingsSectionHeader(t.settings.homeScreen), _buildShowHeroSection(), _buildUseGlobalHubs(), _buildShowServerNameOnHubs(), - // --- Navigation --- SettingsSectionHeader(t.settings.navigation), if (Platform.isAndroid) _buildForceTvMode(), if (PlatformDetector.shouldUseSideNavigation(context)) _buildAlwaysKeepSidebarOpen(), @@ -78,13 +75,11 @@ class _AppearanceSettingsScreenState extends State { if (!PlatformDetector.shouldUseSideNavigation(context)) _buildShowNavBarLabels(), _buildShowUnwatchedCount(), - // --- Window (Windows/Linux only) --- if (Platform.isWindows || Platform.isLinux) ...[ SettingsSectionHeader(t.settings.window), _buildStartInFullscreen(), ], - // --- Content --- SettingsSectionHeader(t.settings.content), _buildLiveTvDefaultFavorites(), _buildHideSpoilers(), @@ -98,8 +93,6 @@ class _AppearanceSettingsScreenState extends State { ); } - // --- Display section --- - Widget _buildThemeSelector() { return Consumer( builder: (context, themeProvider, child) { @@ -251,8 +244,6 @@ class _AppearanceSettingsScreenState extends State { ); } - // --- Home Screen section --- - Widget _buildShowHeroSection() => _buildBoolToggle( icon: Symbols.featured_play_list_rounded, title: t.settings.showHeroSection, @@ -277,8 +268,6 @@ class _AppearanceSettingsScreenState extends State { setter: (p, v) => p.setShowServerNameOnHubs(v), ); - // --- Navigation section --- - Widget _buildAlwaysKeepSidebarOpen() => _buildBoolToggle( icon: Symbols.dock_to_left_rounded, title: t.settings.alwaysKeepSidebarOpen, @@ -311,8 +300,6 @@ class _AppearanceSettingsScreenState extends State { setter: (p, v) => p.setShowUnwatchedCount(v), ); - // --- Content section --- - Widget _buildLiveTvDefaultFavorites() => _buildBoolToggle( icon: Symbols.star_rounded, title: t.settings.liveTvDefaultFavorites, @@ -400,8 +387,6 @@ class _AppearanceSettingsScreenState extends State { ); } - // --- Helpers --- - String _getLanguageDisplayName(AppLocale locale) { switch (locale) { case AppLocale.en: diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index fbb4bbec..d37c4976 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -123,7 +123,6 @@ class _PlaybackSettingsScreenState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - // --- Player --- SettingsSectionHeader(t.settings.player), if (Platform.isAndroid) _buildPlayerBackendSelector(), _buildExternalPlayerTile(), @@ -139,12 +138,10 @@ class _PlaybackSettingsScreenState extends State { _buildBufferSizeSelector(), _buildDefaultQualityTile(), - // --- Subtitles & Config --- SettingsSectionHeader(t.settings.subtitlesAndConfig), _buildSubtitleStylingTile(), if (!Platform.isAndroid || !_useExoPlayer) _buildMpvConfigTile(), - // --- Seek & Timing --- SettingsSectionHeader(t.settings.seekAndTiming), _buildSmallSkipDuration(), _buildLargeSkipDuration(), @@ -152,14 +149,12 @@ class _PlaybackSettingsScreenState extends State { _buildDefaultSleepTimer(), _buildMaxVolume(), - // --- Behavior --- SettingsSectionHeader(t.settings.behavior), if (DiscordRPCService.isAvailable) _buildDiscordRPC(), if (PlatformDetector.shouldActAsRemoteHost(context)) _buildCompanionRemoteServer(), _buildRememberTrackSelections(), if (!isMobile) _buildClickVideoTogglesPlayback(), - // --- Auto-Skip --- SettingsSectionHeader(t.settings.autoSkip), _buildAutoSkipIntro(), _buildAutoSkipCredits(), @@ -174,8 +169,6 @@ class _PlaybackSettingsScreenState extends State { ); } - // --- Player section --- - Widget _buildPlayerBackendSelector() { return SegmentedSetting( icon: Symbols.play_circle_rounded, @@ -369,8 +362,6 @@ class _PlaybackSettingsScreenState extends State { ); } - // --- Subtitles & Config section --- - Widget _buildSubtitleStylingTile() { return ListTile( leading: const AppIcon(Symbols.subtitles_rounded, fill: 1), @@ -395,8 +386,6 @@ class _PlaybackSettingsScreenState extends State { ); } - // --- Seek & Timing section --- - Widget _buildSmallSkipDuration() { return ListTile( leading: const AppIcon(Symbols.replay_10_rounded, fill: 1), @@ -515,8 +504,6 @@ class _PlaybackSettingsScreenState extends State { ); } - // --- Behavior section --- - Widget _buildDiscordRPC() { return SwitchListTile( secondary: const AppIcon(Symbols.chat_rounded, fill: 1), @@ -570,8 +557,6 @@ class _PlaybackSettingsScreenState extends State { ); } - // --- Auto-Skip section --- - Widget _buildAutoSkipIntro() { return SwitchListTile( secondary: const AppIcon(Symbols.fast_forward_rounded, fill: 1), diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 066f6b63..c208d25e 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -168,40 +168,28 @@ class _SettingsScreenState extends State with FocusableTab { ExcludeFocus(child: CustomAppBar(title: Text(t.settings.title), pinned: true)), SliverList( delegate: SliverChildListDelegate([ - // --- Donate (non-store builds only) --- if (DonationService.isEnabled) _buildDonateTile(), - // --- Appearance (navigation tile) --- _buildAppearanceTile(), - // --- Playback (navigation tile) --- _buildPlaybackTile(), - // --- Trackers (unified hub: Trakt + MAL + AniList + Simkl) --- _buildTrackersTile(), - // --- Connections (Jellyfin servers) --- _buildConnectionsSection(), - // --- Profiles (kids mode / multi-user) --- _buildProfilesSection(), - // --- Downloads (inline) --- if (!PlatformDetector.isAppleTV()) _buildDownloadsSection(), - // --- Keyboard Shortcuts (inline, conditional) --- if (_keyboardShortcutsSupported) ...[_buildKeyboardShortcutsSection()], - // --- Advanced (inline) --- _buildAdvancedSection(), - // --- Updates (conditional) --- if (UpdateService.isUpdateCheckEnabled) ...[_buildUpdateSection()], - // --- Backup (hidden on Apple TV — no file picker / storage) --- if (!PlatformDetector.isAppleTV()) _buildBackupSection(), - // --- About --- ListTile( focusNode: _focusTracker.get(_kAbout), leading: const AppIcon(Symbols.info_rounded, fill: 1), @@ -615,8 +603,6 @@ class _SettingsScreenState extends State with FocusableTab { ); } - // --- Dialogs --- - Future _showDownloadLocationDialog() async { final storageService = DownloadStorageService.instance; final isCustom = storageService.isUsingCustomPath(); diff --git a/lib/screens/settings/subtitle_styling_screen.dart b/lib/screens/settings/subtitle_styling_screen.dart index 2e9ccc7b..028f6120 100644 --- a/lib/screens/settings/subtitle_styling_screen.dart +++ b/lib/screens/settings/subtitle_styling_screen.dart @@ -182,7 +182,6 @@ class _SubtitleStylingScreenState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - // --- Text --- SettingsSectionHeader(t.subtitlingStyling.text), ListTile( leading: const AppIcon(Symbols.subtitles_rounded, fill: 1), @@ -279,7 +278,6 @@ class _SubtitleStylingScreenState extends State { }, ), - // --- Border --- SettingsSectionHeader(t.subtitlingStyling.border), ListTile( leading: const AppIcon(Symbols.border_style_rounded, fill: 1), @@ -319,7 +317,6 @@ class _SubtitleStylingScreenState extends State { }), ), - // --- Background --- SettingsSectionHeader(t.subtitlingStyling.background), ListTile( leading: const AppIcon(Symbols.opacity_rounded, fill: 1), diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index fd43e7a4..eb3e1f9e 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -262,12 +262,11 @@ class VideoPlayerScreenState extends State with WidgetsBindin StreamSubscription? _mediaControlsRateSubscription; StreamSubscription? _mediaControlsSeekableSubscription; StreamSubscription>? _serverStatusSubscription; - bool _isReplacingWithVideo = false; // Flag to skip orientation restoration during video-to-video navigation + bool _isReplacingWithVideo = false; bool _isDisposingForNavigation = false; bool _isHandlingBack = false; ScrubPreviewSource? _scrubPreviewSource; - // Live TV channel navigation int _liveChannelIndex = -1; String? _liveChannelName; MediaServerClient? _liveClient; @@ -287,7 +286,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin // the Jellyfin started/progress/stopped transition. JellyfinLiveSessionTracker _jellyfinLiveSession = JellyfinLiveSessionTracker(); - // Live TV time-shift CaptureBuffer? _captureBuffer; int? _programBeginsAt; double _streamStartEpoch = 0; @@ -299,16 +297,13 @@ class VideoPlayerScreenState extends State with WidgetsBindin int _liveStreamFallbackLevel = 0; bool _isRetryingLiveStream = false; - // Auto-play next episode Timer? _autoPlayTimer; int _autoPlayCountdown = 5; bool _completionTriggered = false; - // Play Next dialog focus nodes (for TV D-pad navigation) late final FocusNode _playNextCancelFocusNode; late final FocusNode _playNextConfirmFocusNode; - // "Still watching?" prompt (sleep timer) bool _showStillWatchingPrompt = false; int _stillWatchingCountdown = 30; Timer? _stillWatchingTimer; @@ -345,7 +340,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin ((Platform.isIOS || Platform.isMacOS) && _autoPipEnabled) || (Platform.isAndroid && _androidAutoPipTransitionInFlight); - // Services MediaControlsManager? _mediaControlsManager; PlaybackProgressTracker? _progressTracker; VideoFilterManager? _videoFilterManager; @@ -354,10 +348,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin AmbientLightingService? _ambientLightingService; final EpisodeNavigationService _episodeNavigation = EpisodeNavigationService(); - // Watch Together provider reference (stored early to use in dispose) WatchTogetherProvider? _watchTogetherProvider; - // Companion remote state (stored early for use in dispose) CompanionRemoteProvider? _companionRemoteProvider; VoidCallback? _savedOnHome; @@ -373,12 +365,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin ScrubFrame? _getThumbnailData(Duration time) => _scrubPreviewSource?.getFrame(time); - final ValueNotifier _isBuffering = ValueNotifier(false); // Track if video is currently buffering - final ValueNotifier _hasFirstFrame = ValueNotifier(false); // Track if first video frame has rendered - final ValueNotifier _isExiting = ValueNotifier(false); // Track if navigating away (for black overlay) - final ValueNotifier _controlsVisible = ValueNotifier( - true, - ); // Track if video controls are visible (for popup positioning) + final ValueNotifier _isBuffering = ValueNotifier(false); + final ValueNotifier _hasFirstFrame = ValueNotifier(false); + final ValueNotifier _isExiting = ValueNotifier(false); + final ValueNotifier _controlsVisible = ValueNotifier(true); @override void initState() { @@ -388,17 +378,14 @@ class VideoPlayerScreenState extends State with WidgetsBindin _activeId = widget.metadata.id; _activeMediaIndex = widget.selectedMediaIndex; - // Transcode session identifiers — reused across quality/version/audio - // switches so the server-side transcode session is preserved. + // Reused across quality/version/audio switches so the server-side + // transcode session is preserved. _playbackSessionIdentifier = widget.reusedSessionIdentifier ?? generateSessionIdentifier(); _playbackTranscodeSessionId = widget.reusedTranscodeSessionId ?? generateSessionIdentifier(); _selectedAudioStreamId = widget.selectedAudioStreamId; _effectiveIsOffline = widget.isOffline; - // Quality preset is resolved later when the SettingsProvider is available; - // see _resolveQualityPreset() called from _initializePlayer. _selectedQualityPreset = widget.selectedQualityPreset ?? TranscodeQualityPreset.original; - // Initialize live TV channel tracking _liveChannelIndex = widget.liveCurrentChannelIndex ?? -1; _liveChannelName = widget.liveChannelName; _liveClient = widget.liveClient; @@ -411,11 +398,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin _jellyfinLiveSession = JellyfinLiveSessionTracker(playSessionId: widget.liveSessionIdentifier); } - // Initialize Play Next dialog focus nodes _playNextCancelFocusNode = FocusNode(debugLabel: 'PlayNextCancel'); _playNextConfirmFocusNode = FocusNode(debugLabel: 'PlayNextConfirm'); - // Initialize "Still watching?" dialog focus nodes _stillWatchingPauseFocusNode = FocusNode(debugLabel: 'StillWatchingPause'); _stillWatchingContinueFocusNode = FocusNode(debugLabel: 'StillWatchingContinue'); @@ -437,7 +422,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin appLogger.d('Preferred subtitle track: $subtitleDesc'); } - // Update current item in playback state provider try { final playbackState = context.read(); @@ -459,22 +443,17 @@ class VideoPlayerScreenState extends State with WidgetsBindin } }); } catch (e) { - // Provider might not be available yet during initialization appLogger.d('Deferred playback state update (provider not ready)', error: e); } - // Register app lifecycle observer WidgetsBinding.instance.addObserver(this); - // Wire companion remote playback callbacks _setupCompanionRemoteCallbacks(); - // Show "Still watching?" prompt when sleep timer fires _sleepTimerSubscription = SleepTimerService().onPrompt.listen((_) { if (mounted) _showStillWatchingDialog(); }); - // Initialize player asynchronously with buffer size from settings _initializePlayer(); } @@ -682,7 +661,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (mounted) { setState(() => _playerInitializationError = null); } - // Load buffer size from settings final settingsService = await SettingsService.getInstance(); _videoPlayerNavigationEnabled = settingsService.read(SettingsService.videoPlayerNavigationEnabled); _autoPipEnabled = settingsService.read(SettingsService.autoPip); @@ -692,14 +670,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin final debugLoggingEnabled = settingsService.read(SettingsService.enableDebugLogging); final useExoPlayer = settingsService.read(SettingsService.useExoPlayer); - // Initialize Windows display mode service. if (Platform.isWindows) { _displayModeService = DisplayModeService(settingsService, FullscreenStateManager()); await _displayModeService!.syncWithNative(); FullscreenStateManager().addListener(_onFullscreenChanged); } - // Create player (on Android, uses ExoPlayer by default, MPV as fallback) player = Player(useExoPlayer: useExoPlayer); _playerBackendLabel = player!.playerType; @@ -771,7 +747,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (bufferSizeMB > 0) { final bufferSizeBytes = bufferSizeMB * 1024 * 1024; await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); - // Set back-buffer to 1/4 of forward buffer final backBytes = bufferSizeBytes ~/ 4; await player!.setProperty('demuxer-max-back-bytes', backBytes.toString()); } @@ -791,7 +766,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin autoBackMB = 48; } if (bufferSizeMB == 0) { - // Auto mode: cap both forward and back buffer based on heap int autoForwardMB; if (heapMB <= 256) { autoForwardMB = 32; @@ -813,7 +787,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin await player!.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); await player!.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); - // Subtitle styling await player!.setProperty('sub-font-size', settingsService.read(SettingsService.subtitleFontSize).toString()); await player!.setProperty('sub-color', settingsService.read(SettingsService.subtitleTextColor)); await player!.setProperty('sub-border-size', settingsService.read(SettingsService.subtitleBorderSize).toString()); @@ -833,7 +806,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin await player!.setProperty('sub-ass-video-aspect-override', '1'); await player!.setProperty('sub-pos', settingsService.read(SettingsService.subtitlePosition).toString()); - // Platform-specific settings if (Platform.isIOS) { await player!.setProperty('audio-exclusive', 'yes'); } @@ -851,26 +823,22 @@ class VideoPlayerScreenState extends State with WidgetsBindin await player!.setProperty('hdr-enabled', enableHDR ? 'yes' : 'no'); } - // Apply audio sync offset final audioSyncOffset = settingsService.read(SettingsService.audioSyncOffset); if (audioSyncOffset != 0) { final offsetSeconds = audioSyncOffset / 1000.0; await player!.setProperty('audio-delay', offsetSeconds.toString()); } - // Apply subtitle sync offset final subtitleSyncOffset = settingsService.read(SettingsService.subtitleSyncOffset); if (subtitleSyncOffset != 0) { final offsetSeconds = subtitleSyncOffset / 1000.0; await player!.setProperty('sub-delay', offsetSeconds.toString()); } - // Apply audio normalization (loudnorm filter) if (settingsService.read(SettingsService.audioNormalization)) { await player!.setProperty('af', 'loudnorm=I=-14:TP=-3:LRA=4'); } - // Apply custom MPV config entries final customMpvConfig = SettingsService.parseMpvConfigText(settingsService.read(SettingsService.mpvConfigText)); for (final entry in customMpvConfig.entries) { try { @@ -881,15 +849,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } - // Set max volume limit for volume boost final maxVolume = settingsService.read(SettingsService.maxVolume); await player!.setProperty('volume-max', maxVolume.toString()); - // Apply saved volume (clamped to max volume) final savedVolume = settingsService.read(SettingsService.volume).clamp(0.0, maxVolume.toDouble()); unawaited(player!.setVolume(savedVolume)); - // Notify that player is ready if (mounted) { setState(() { _isPlayerInitialized = true; @@ -906,7 +871,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin appLogger.d('Wakelock enabled for video playback'); } - // Get the video URL and start playback await _startPlayback(); // Set fullscreen mode and orientation based on rotation lock setting @@ -941,7 +905,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (_positionSubscription != null) _positionSubscription!.cancel(), ]); - // Listen to playback state changes _playingSubscription = player!.streams.playing.listen(_onPlayingStateChanged); // Listen to completion. When mpv emits completed=false (file-loaded after a @@ -955,7 +918,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin _onVideoCompleted(done); }); - // Listen to MPV errors _errorSubscription = player!.streams.error.listen(_onPlayerError); // warn is included so we can catch ffmpeg's "HTTP error 500" line in @@ -964,12 +926,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin .where((log) => const {PlayerLogLevel.fatal, PlayerLogLevel.error, PlayerLogLevel.warn}.contains(log.level)) .listen(_onPlayerLog); - // Listen for backend switched event (ExoPlayer -> MPV fallback on Android) if (Platform.isAndroid && useExoPlayer) { _backendSwitchedSubscription = player!.streams.backendSwitched.listen((_) => _onBackendSwitched()); } - // Listen to buffering state _bufferingSubscription = player!.streams.buffering.listen((isBuffering) { _isBuffering.value = isBuffering; }); @@ -994,7 +954,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin } } - // Listen to playback restart to detect first frame ready _playbackRestartSubscription = player!.streams.playbackRestart.listen((_) async { _lastLogError = null; _sawServer500 = false; @@ -1003,12 +962,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin _hasFirstFrame.value = true; unawaited(Sentry.addBreadcrumb(Breadcrumb(message: 'First frame ready', category: 'player'))); - // Apply frame rate matching on Android if enabled if (Platform.isAndroid && settingsService.read(SettingsService.matchContentFrameRate)) { await _applyFrameRateMatching(); } - // Apply Windows display mode matching (refresh rate, HDR) if (Platform.isWindows && _displayModeService != null) { await _applyWindowsDisplayMatching(); } @@ -1016,7 +973,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin _trackManager?.onPlaybackRestart(); }); - // Listen to position for completion detection (fallback for unreliable MPV events) int? lastObservedPositionMs; _positionSubscription = player!.streams.position.listen((position) { // Fallback for cases where playbackRestart doesn't fire (observed on @@ -2387,10 +2343,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin @override void dispose() { - // Unregister app lifecycle observer WidgetsBinding.instance.removeObserver(this); - // Clean up companion remote playback callbacks _cleanupCompanionRemoteCallbacks(); // Notify Watch Together guests that host is exiting the player @@ -2403,10 +2357,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin _watchTogetherProvider!.notifyHostExitedPlayer(); } - // Detach from Watch Together session _detachFromWatchTogetherSession(); - // Dispose value notifiers _isBuffering.dispose(); _hasFirstFrame.dispose(); _isExiting.dispose(); @@ -2422,14 +2374,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin _sendLiveTimeline('stopped'); _stopLiveTimelineUpdates(); - // Remove PiP state listener, clear callbacks, disable auto-PiP, and dispose video filter manager _videoPIPManager?.isPipActive.removeListener(_onPipStateChanged); _videoPIPManager?.onBeforeEnterPip = null; _videoPIPManager?.disableAutoPip(); PipService.onAutoPipEntering = null; _videoFilterManager?.dispose(); - // Release cached scrub-thumbnail data (BIF or trickplay) _scrubPreviewSource?.dispose(); // Mark sleep timer for restart if truly exiting (not episode transition) @@ -2437,7 +2387,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin SleepTimerService().markNeedsRestart(); } - // Cancel stream subscriptions _playingSubscription?.cancel(); _completedSubscription?.cancel(); _errorSubscription?.cancel(); @@ -2455,34 +2404,26 @@ class VideoPlayerScreenState extends State with WidgetsBindin _mediaControlsSeekableSubscription?.cancel(); _serverStatusSubscription?.cancel(); - // Cancel auto-play timer _autoPlayTimer?.cancel(); - // Cancel still watching timer _stillWatchingTimer?.cancel(); - // Dispose Play Next dialog focus nodes _playNextCancelFocusNode.dispose(); _playNextConfirmFocusNode.dispose(); - // Dispose "Still watching?" dialog focus nodes _stillWatchingPauseFocusNode.dispose(); _stillWatchingContinueFocusNode.dispose(); - // Dispose screen-level focus node _screenFocusNode.removeListener(_onScreenFocusChanged); _screenFocusNode.dispose(); - // Clear media controls and dispose manager _mediaControlsManager?.clear(); _mediaControlsManager?.dispose(); - // Clear Discord Rich Presence + send Trakt stop scrobble DiscordRPCService.instance.stopPlayback(); TraktScrobbleService.instance.stopPlayback(); TrackerCoordinator.instance.stopPlayback(); - // Clean up Windows display mode service if (Platform.isWindows && _displayModeService != null) { FullscreenStateManager().removeListener(_onFullscreenChanged); } @@ -2502,7 +2443,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin player!.abandonAudioFocus(); } - // Disable wakelock when leaving the video player _setWakelock(false); appLogger.d('Wakelock disabled'); @@ -2513,10 +2453,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Restore orientation based on cached device type (no context needed) try { if (_isPhone) { - // Phone: portrait only SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); } else { - // Tablet/Desktop: all orientations SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, @@ -2820,7 +2758,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin bool _isSwitchingChannel = false; - /// Switch to an adjacent live TV channel (delta: +1 for next, -1 for previous) /// Start periodic timeline heartbeats for live TV transcode session. void _startLiveTimelineUpdates() { final generation = ++_liveTimelineGeneration; @@ -3203,8 +3140,6 @@ class VideoPlayerScreenState extends State with WidgetsBindin }); } - // -- "Still watching?" prompt -- - void _showStillWatchingDialog() { // Don't show if auto-play dialog is already visible if (_showPlayNextDialog) return; diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 3c4bf9be..0500c1ca 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -30,8 +30,6 @@ import 'package:sentry_flutter/sentry_flutter.dart'; typedef MediaClientResolver = MediaServerClient? Function(String serverId, {String? clientScopeId}); -/// Context for a download that's been enqueued with background_downloader. -/// Carries metadata needed between enqueue and completion callback. class _DownloadContext { final MediaItem metadata; final DownloadQueueItem queueItem; @@ -59,15 +57,12 @@ class DownloadManagerService { final DownloadStorageService _storageService; final MediaServerHttpClient _http; - // Stream controller for download progress updates final _progressController = StreamController.broadcast(); Stream get progressStream => _progressController.stream; - // Stream controller for deletion progress updates final _deletionProgressController = StreamController.broadcast(); Stream get deletionProgressStream => _deletionProgressController.stream; - // Context for downloads enqueued in this session final Map _pendingDownloadContext = {}; // Items recovered with video complete but supplementary downloads missing @@ -80,7 +75,6 @@ class DownloadManagerService { OfflineModeSource? _offlineSource; - // background_downloader state bool _fileDownloaderInitialized = false; static const _downloadGroup = 'video_downloads'; static const _maxAppRetries = 3; @@ -112,8 +106,6 @@ class DownloadManagerService { int _consecutiveQueueFailures = 0; static const _maxConsecutiveFailures = 3; - /// Public method to check if downloads should be blocked due to cellular-only setting - /// Can be used by DownloadProvider to show user-friendly error static Future shouldBlockDownloadOnCellular() async { final List connectivity; try { @@ -149,7 +141,6 @@ class DownloadManagerService { _storageService = storageService, _http = http ?? httpClient; - /// Register a callback to resolve the correct [MediaServerClient] for a given serverId. void setClientResolver(MediaClientResolver resolver) { _clientResolver = resolver; } @@ -383,10 +374,9 @@ class DownloadManagerService { progressBar: true, ); - // Configure native holding queue: max 1 concurrent (Plex server limitation) + // Plex servers can reject concurrent media downloads. await FileDownloader().configure(globalConfig: (Config.holdingQueue, (1, 1, 1))); - // Track tasks for persistence across app restarts await FileDownloader().trackTasks(); // Deliver status updates from iOS background-to-foreground transitions await FileDownloader().resumeFromBackground(); @@ -679,7 +669,6 @@ class DownloadManagerService { ]); } - /// Queue a download for a media item Future queueDownload({ required MediaItem metadata, required MediaServerClient client, @@ -690,7 +679,6 @@ class DownloadManagerService { }) async { final globalKey = metadata.globalKey; - // Check if already downloading or completed final existing = await _database.getDownloadedMedia(globalKey); if (existing != null && (existing.status == DownloadStatus.downloading.index || existing.status == DownloadStatus.completed.index)) { @@ -698,7 +686,6 @@ class DownloadManagerService { return; } - // Insert into database await _database.insertDownload( serverId: metadata.serverId!, clientScopeId: client.cacheServerId == metadata.serverId ? null : client.cacheServerId, @@ -716,7 +703,6 @@ class DownloadManagerService { // cache is warm and falls back to the existing entry on network error. await _pinMetadataForOffline(client, metadata); - // Add to queue await _database.addToQueue( mediaGlobalKey: globalKey, priority: priority, @@ -726,7 +712,6 @@ class DownloadManagerService { _emitProgress(globalKey, DownloadStatus.queued, 0); - // Start processing if not already unawaited(_processQueue(client)); } @@ -1219,7 +1204,6 @@ class DownloadManagerService { } } - // Store video path in DB await _database.updateVideoFilePath(globalKey, storedPath); appLogger.d('Video download completed for $globalKey'); @@ -1229,7 +1213,6 @@ class DownloadManagerService { final client = ctx?.client ?? await _getClientForDownloadKey(globalKey); final showYear = ctx?.showYear; - // Get queue item settings (still in drift at this point) final queueItem = ctx?.queueItem ?? await (_database.select( @@ -1278,7 +1261,6 @@ class DownloadManagerService { } } - /// Resolve metadata from cache using a globalKey Future _resolveMetadata(String globalKey) async { final parsed = parseGlobalKey(globalKey); if (parsed == null) return null; @@ -1292,7 +1274,6 @@ class DownloadManagerService { return (await _lookupMetadata(serverId, grandparentRatingKey, clientScopeId: clientScopeId))?.year; } - /// Re-derive the SAF file URI from metadata (for recovery when context is lost) Future _resolveSafStoredPath(MediaItem metadata, String ext, int? showYear) async { final safBaseUri = _storageService.safBaseUri; if (safBaseUri == null) return null; @@ -1334,8 +1315,6 @@ class DownloadManagerService { return _fetchShowYear(serverId, metadata.grandparentId, clientScopeId: clientScopeId); } - /// Download artwork for a media item using hash-based storage - /// Downloads all artwork types: thumb/poster, clearLogo, and background art Future _downloadArtwork(String globalKey, MediaItem metadata, MediaServerClient client) async { if (metadata.serverId == null) return; @@ -1348,7 +1327,6 @@ class DownloadManagerService { await _downloadSingleArtwork(serverId, spec); } - // Store thumb reference in database (primary artwork for display) final storedThumbPath = metadata.thumbPath == null ? null : artworkStorageKey(metadata.thumbPath!); await _database.updateArtworkPaths(globalKey: globalKey, thumbPath: storedThumbPath); @@ -1605,17 +1583,14 @@ class DownloadManagerService { await _database.removeFromQueue(globalKey); } - /// Delete a downloaded item and its files Future deleteDownload(String globalKey) async { _cancelDownloadTimers(globalKey); - // Cancel if actively downloading via background_downloader final bgTaskId = await _database.getBgTaskId(globalKey); if (bgTaskId != null) { await FileDownloader().cancelTaskWithId(bgTaskId); } _pendingDownloadContext.remove(globalKey); - // Delete files from storage final parsed = parseGlobalKey(globalKey); if (parsed == null) { await _database.deleteDownload(globalKey); @@ -1636,24 +1611,18 @@ class DownloadManagerService { return; } - // Determine total items to delete final totalItems = await _getTotalItemsToDelete(metadata, serverId, clientScopeId: clientScopeId); - // Emit initial progress _emitDeletionProgress( DeletionProgress(globalKey: globalKey, itemTitle: metadata.displayTitle, currentItem: 0, totalItems: totalItems), ); - // Delete files from storage (with progress updates) await _deleteMediaFilesWithMetadata(serverId, ratingKey, clientScopeId: clientScopeId); - // Delete from API cache await _deleteForItemByServer(serverId, ratingKey, clientScopeId: clientScopeId); - // Delete from database await _database.deleteDownload(globalKey); - // Emit completion _emitDeletionProgress( DeletionProgress( globalKey: globalKey, @@ -1664,7 +1633,6 @@ class DownloadManagerService { ); } - /// Emit deletion progress update void _emitDeletionProgress(DeletionProgress progress) { if (_disposed) return; _deletionProgressController.add(progress); @@ -1687,12 +1655,10 @@ class DownloadManagerService { } } - /// Delete media files using metadata to find correct paths Future _deleteMediaFilesWithMetadata(String serverId, String ratingKey, {String? clientScopeId}) async { try { final gk = buildGlobalKey(serverId, ratingKey); final downloadRecord = await _database.getDownloadedMedia(gk); - // Get metadata from API cache final scopeId = clientScopeId ?? downloadRecord?.clientScopeId; final metadata = await _lookupMetadata(serverId, ratingKey, clientScopeId: scopeId); @@ -1777,7 +1743,6 @@ class DownloadManagerService { return; } - // Build a set of all chapter thumb paths used by OTHER items on this server final otherItems = await _database.getDownloadsByServerId(serverId); final inUseThumbPaths = {}; for (final item in otherItems) { @@ -1801,7 +1766,6 @@ class DownloadManagerService { continue; } - // Get artwork file path and delete final artworkPath = await _storageService.getArtworkPathFromThumb(serverId, thumbPath); if (await _deleteFileIfExists(File(artworkPath), 'chapter thumbnail')) { deletedCount++; @@ -1819,7 +1783,6 @@ class DownloadManagerService { } } - /// Delete episode files Future _deleteEpisodeFiles(MediaItem episode, String serverId, {String? clientScopeId}) async { try { final parentMetadata = episode.grandparentId != null @@ -1827,7 +1790,6 @@ class DownloadManagerService { : null; final showYear = parentMetadata?.year; - // Delete video file final videoPathTemplate = await _storageService.getEpisodeVideoPath(episode, 'tmp', showYear: showYear); final videoPathWithoutExt = videoPathTemplate.substring(0, videoPathTemplate.lastIndexOf('.')); final actualVideoFile = await _findFileWithAnyExtension(videoPathWithoutExt); @@ -1837,21 +1799,17 @@ class DownloadManagerService { await _deleteFileIfExists(File('${actualVideoFile.path}.part'), 'partial download'); } - // Delete thumbnail final thumbPath = await _storageService.getEpisodeThumbnailPath(episode, showYear: showYear); await _deleteFileIfExists(File(thumbPath), 'episode thumbnail'); - // Delete subtitles directory final subsDir = await _storageService.getEpisodeSubtitlesDirectory(episode, showYear: showYear); if (await subsDir.exists()) { await subsDir.delete(recursive: true); appLogger.i('Deleted episode subtitles: ${subsDir.path}'); } - // Delete chapter thumbnails (with reference counting) await _deleteChapterThumbnails(serverId, episode.id, clientScopeId: clientScopeId); - // Clean up parent directories if empty await _cleanupEmptyDirectories(episode, showYear); // Safety net: verify the actual DB-recorded file is gone @@ -1861,7 +1819,6 @@ class DownloadManagerService { } } - /// Delete season files Future _deleteSeasonFiles(MediaItem season, String serverId, {String? clientScopeId}) async { try { final parentMetadata = season.parentId != null @@ -1869,7 +1826,6 @@ class DownloadManagerService { : null; final showYear = parentMetadata?.year; - // Get all episodes in this season final episodesInSeason = await _database.getEpisodesBySeason(season.id, serverId: serverId); appLogger.d('Deleting ${episodesInSeason.length} episodes in season ${season.id}'); @@ -1946,10 +1902,8 @@ class DownloadManagerService { } } - /// Delete show files Future _deleteShowFiles(MediaItem show, String serverId, {String? clientScopeId}) async { try { - // Get all episodes in this show final episodesInShow = await _database.getEpisodesByShow(show.id, serverId: serverId); appLogger.d('Deleting ${episodesInShow.length} episodes in show ${show.id}'); @@ -1971,7 +1925,6 @@ class DownloadManagerService { } } - /// Delete movie files Future _deleteMovieFiles(MediaItem movie, String serverId, {String? clientScopeId}) async { try { final movieDir = await _storageService.getMovieDirectory(movie); @@ -1980,7 +1933,6 @@ class DownloadManagerService { appLogger.i('Deleted movie directory: ${movieDir.path}'); } - // Delete chapter thumbnails (with reference counting) await _deleteChapterThumbnails(serverId, movie.id, clientScopeId: clientScopeId); // Safety net: verify the actual DB-recorded file is gone @@ -2161,7 +2113,6 @@ class DownloadManagerService { appLogger.w('Safety net: video still exists after metadata deletion, deleting: $videoPath'); await videoFile.delete(); - // Clean up .part file and subtitles directory alongside video await _deleteFileIfExists(File('$videoPath.part'), 'partial download'); final subsPath = videoPath.replaceAll(RegExp(r'\.[^.]+$'), '_subs'); final subsDir = Directory(subsPath); @@ -2238,29 +2189,24 @@ class DownloadManagerService { } } - /// Check if season artwork is in use Future _isSeasonArtworkInUse(MediaItem episode, int? _) async { final seasonKey = episode.parentId; if (seasonKey == null) return false; final otherEpisodes = await _database.getEpisodesBySeason(seasonKey); - // Check if any episodes besides this one return otherEpisodes.any((e) => e.globalKey != episode.globalKey); } - /// Check if show artwork is in use Future _isShowArtworkInUse(MediaItem metadata, int? _) async { final showKey = metadata.grandparentId ?? metadata.parentId ?? metadata.id; // Use targeted query instead of full table scan final showEpisodes = await _database.getEpisodesByShow(showKey); - // Check if any episodes belong to this show besides the current item return showEpisodes.any((item) => item.globalKey != metadata.globalKey); } - /// Find file with any extension Future _findFileWithAnyExtension(String pathWithoutExt) async { final dir = Directory(path.dirname(pathWithoutExt)); final baseName = path.basename(pathWithoutExt); @@ -2293,10 +2239,8 @@ class DownloadManagerService { final videoDeleted = await _deleteFileIfExists(videoFile, 'video file'); if (videoDeleted) { - // Delete .part file from interrupted downloads await _deleteFileIfExists(File('$videoPath.part'), 'partial download'); - // Delete subtitle directory final subsPath = videoPath.replaceAll(RegExp(r'\.[^.]+$'), '_subs'); final subsDir = Directory(subsPath); if (await subsDir.exists()) { @@ -2304,7 +2248,6 @@ class DownloadManagerService { appLogger.i('Deleted subtitles: $subsPath'); } - // Clean up empty parent directories await _cleanupEmptyParentDirectories(videoFile.parent); } } @@ -2324,12 +2267,10 @@ class DownloadManagerService { } } - /// Get all downloaded media items (for loading persisted data) Future> getAllDownloads() { return _database.select(_database.downloadedMedia).get(); } - /// Get a specific downloaded media item by globalKey Future getDownloadedMedia(String globalKey) { return _database.getDownloadedMedia(globalKey); } diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart index 13cf61fb..a14d41c0 100644 --- a/lib/services/download_storage_service.dart +++ b/lib/services/download_storage_service.dart @@ -38,34 +38,26 @@ class DownloadStorageService { Directory? _baseDownloadsDir; String? _artworkDirectoryPath; - // Custom path configuration SettingsService? _settingsService; String? _customDownloadPath; String _customPathType = 'file'; - /// Check if currently using SAF mode (Android only) bool get isUsingSaf => Platform.isAndroid && _customPathType == 'saf' && _customDownloadPath != null; - /// Get the SAF base URI (only valid when isUsingSaf is true) String? get safBaseUri => isUsingSaf ? _customDownloadPath : null; - /// Get artwork directory path (cached, synchronous after first call) String? get artworkDirectoryPath => _artworkDirectoryPath; - /// Initialize with settings service (call during app startup) Future initialize(SettingsService settingsService) async { _settingsService = settingsService; _customDownloadPath = settingsService.read(SettingsService.customDownloadPath); _customPathType = settingsService.read(SettingsService.customDownloadPathType) ?? 'file'; - // Reset cached directories to force recalculation _baseDownloadsDir = null; _artworkDirectoryPath = null; - // Eagerly initialize artwork directory for sync access await getArtworkDirectory(); } - /// Refresh custom path from settings (call when settings change) Future refreshCustomPath() async { if (_settingsService != null) { _customDownloadPath = _settingsService!.read(SettingsService.customDownloadPath); @@ -93,10 +85,8 @@ class DownloadStorageService { return 'S${season}E$ep - $episodeName'; } - /// Check if using custom download path bool isUsingCustomPath() => _customDownloadPath != null; - /// Get current download path for display in settings Future getCurrentDownloadPathDisplay() async { if (_customDownloadPath != null) { return _customDownloadPath!; @@ -105,7 +95,6 @@ class DownloadStorageService { return dir.path; } - /// Check if a directory is writable Future isDirectoryWritable(Directory dir) async { try { if (!await dir.exists()) { @@ -121,11 +110,9 @@ class DownloadStorageService { } } - /// Initialize and get base downloads directory Future getDownloadsDirectory() async { if (_baseDownloadsDir != null) return _baseDownloadsDir!; - // Check for custom path first (file type only - SAF handled differently) if (_customDownloadPath != null && _customPathType == 'file') { final customDir = Directory(_customDownloadPath!); if (await isDirectoryWritable(customDir)) { @@ -135,7 +122,6 @@ class DownloadStorageService { // Fall through to default if custom path is not writable } - // Default path logic final baseDir = await _getBaseAppDir(); _baseDownloadsDir = await _ensureDirectoryExists(Directory(path.join(baseDir.path, 'downloads'))); return _baseDownloadsDir!; @@ -176,7 +162,6 @@ class DownloadStorageService { /// Example: artwork/a1b2c3d4e5f6.jpg String? getArtworkPathSync(String serverId, String thumbPath) { if (_artworkDirectoryPath == null) return null; - // Create hash from serverId:thumbPath for deduplication final hash = _hashArtworkPath(serverId, thumbPath); return path.join(_artworkDirectoryPath!, '$hash.jpg'); } @@ -189,7 +174,6 @@ class DownloadStorageService { return path.join(artworkDir.path, '$hash.jpg'); } - /// Check if artwork already exists (for deduplication) Future artworkExists(String serverId, String thumbPath) async { final artworkPath = await getArtworkPathFromThumb(serverId, thumbPath); return File(artworkPath).exists(); @@ -201,19 +185,16 @@ class DownloadStorageService { return md5.convert(utf8.encode(combined)).toString(); } - /// Get directory for a specific media item Future getMediaDirectory(String serverId, String ratingKey) async { final baseDir = await getDownloadsDirectory(); return _ensureDirectoryExists(Directory(path.join(baseDir.path, serverId, ratingKey))); } - /// Get video file path Future getVideoFilePath(String serverId, String ratingKey, String extension) async { final mediaDir = await getMediaDirectory(serverId, ratingKey); return path.join(mediaDir.path, 'video.$extension'); } - /// Get subtitles directory Future getSubtitlesDirectory(String serverId, String ratingKey) async { final mediaDir = await getMediaDirectory(serverId, ratingKey); final subtitlesDir = Directory(path.join(mediaDir.path, 'subtitles')); @@ -223,16 +204,11 @@ class DownloadStorageService { return subtitlesDir; } - /// Get subtitle file path Future getSubtitlePath(String serverId, String ratingKey, int trackId, String extension) async { final subtitlesDir = await getSubtitlesDirectory(serverId, ratingKey); return path.join(subtitlesDir.path, '$trackId.$extension'); } - // ============================================================ - // USER-FRIENDLY PATH METHODS (for Files app visibility) - // ============================================================ - /// Sanitize a filename by removing invalid filesystem characters String _sanitizeFileName(String name) { // Remove invalid filesystem characters: < > : " / \ | ? * @@ -263,7 +239,6 @@ class DownloadStorageService { return year != null ? '$sanitized ($year)' : sanitized; } - /// Get the folder name for a movie: "Movie Name (YYYY)" String _getMovieFolderName(MediaItem movie) { return _formatTitleWithYear(movie.title!, movie.year); } @@ -276,7 +251,6 @@ class DownloadStorageService { return _formatTitleWithYear(title, year); } - /// Get movie directory: downloads/Movies/{Movie Name} ({Year})/ Future getMovieDirectory(MediaItem movie) async { final baseDir = await getDownloadsDirectory(); final movieFolder = _getMovieFolderName(movie); @@ -336,21 +310,18 @@ class DownloadStorageService { return _ensureDirectoryExists(Directory(path.join(base.seasonDirPath, '${base.fileName}_subs'))); } - /// Get episode subtitle path /// [showYear]: Pass the show's premiere year (not episode year) Future getEpisodeSubtitlePath(MediaItem episode, int trackId, String extension, {int? showYear}) async { final subsDir = await getEpisodeSubtitlesDirectory(episode, showYear: showYear); return path.join(subsDir.path, '$trackId.$extension'); } - /// Get subtitles directory for movie Future getMovieSubtitlesDirectory(MediaItem movie) async { final movieDir = await getMovieDirectory(movie); final baseName = _getMovieFolderName(movie); return _ensureDirectoryExists(Directory(path.join(movieDir.path, '${baseName}_subs'))); } - /// Get movie subtitle path Future getMovieSubtitlePath(MediaItem movie, int trackId, String extension) async { final subsDir = await getMovieSubtitlesDirectory(movie); return path.join(subsDir.path, '$trackId.$extension'); @@ -375,14 +346,12 @@ class DownloadStorageService { } if (result != absolutePath) return result; - // Already relative or from a different base - return as-is return absolutePath; } /// Convert a relative file path to an absolute path (for file operations) /// Reconstructs the full path using the current app documents directory. Future toAbsolutePath(String relativePath) async { - // If it's already an absolute path, return as-is if (path.isAbsolute(relativePath)) { return relativePath; } @@ -463,13 +432,10 @@ class DownloadStorageService { return fallback; } - /// Get path components for SAF based on media type - /// Returns list of directory names to create under the SAF base List getMovieSafPathComponents(MediaItem movie) { return ['Movies', _getMovieFolderName(movie)]; } - /// Get path components for episode SAF storage List getEpisodeSafPathComponents(MediaItem episode, {int? showYear}) { final showFolder = _getShowFolderName(episode, showYear: showYear); final seasonNum = padNumber(episode.parentIndex ?? 0, 2); @@ -489,12 +455,10 @@ class DownloadStorageService { return ['TV Shows', showFolder, 'Season $seasonNum']; } - /// Get SAF file name for a movie String getMovieSafFileName(MediaItem movie, String extension) { return '${_getMovieFolderName(movie)}.$extension'; } - /// Get SAF file name for an episode String getEpisodeSafFileName(MediaItem episode, String extension) { final fileName = _formatEpisodeFileName(episode); return '$fileName.$extension'; @@ -503,7 +467,6 @@ class DownloadStorageService { /// Get the extension-less episode filename used for SAF lookups. String getEpisodeSafBaseName(MediaItem episode) => _formatEpisodeFileName(episode); - /// Check if a path is a SAF content URI bool isSafUri(String storedPath) { return storedPath.startsWith('content://'); } @@ -513,7 +476,6 @@ class DownloadStorageService { /// For file paths, ensures the path is absolute Future getReadablePath(String storedPath) async { if (isSafUri(storedPath)) { - // SAF content:// URIs are already readable by media players return storedPath; } return await ensureAbsolutePath(storedPath); diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index d36fe19f..a37a6bfd 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -372,7 +372,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return; } - // Parse libraries from the library provider final libraries = []; final epg = <({String identifier, String gridEndpoint})>[]; @@ -554,11 +553,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return ConnectionTestResult(success: true, latencyMs: avgLatency); } - // ============================================================================ - // API Response Parsing Helpers - // ============================================================================ - - /// Extract MediaContainer from API response Map? _getMediaContainer(MediaServerResponse response) { if (response.data is Map && response.data.containsKey('MediaContainer')) { return response.data['MediaContainer']; @@ -566,15 +560,11 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return null; } - /// Tag a PlexMetadataDto with this client's serverId and serverName PlexMetadataDto _tagMetadata(PlexMetadataDto metadata) => metadata.copyWith(serverId: serverId, serverName: serverName); - /// Create and tag a PlexMetadataDto from JSON PlexMetadataDto _createTaggedMetadata(Map json) => _tagMetadata(PlexMetadataDto.fromJson(json)); - /// Extract list of PlexMetadataDto from response - /// Automatically tags all items with this client's serverId and serverName List _extractMetadataList(MediaServerResponse response) { final container = _getMediaContainer(response); if (container != null && container['Metadata'] != null) { @@ -583,7 +573,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return []; } - /// Extract first metadata JSON from response (returns raw Map or null) Map? _getFirstMetadataJson(MediaServerResponse response) { final container = _getMediaContainer(response); if (container != null && container['Metadata'] != null && (container['Metadata'] as List).isNotEmpty) { @@ -592,7 +581,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return null; } - /// Generic helper to extract and map Directory list from response List _extractDirectoryList(MediaServerResponse response, T Function(Map) fromJson) { final container = _getMediaContainer(response); if (container != null && container['Directory'] != null) { @@ -601,7 +589,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return []; } - /// Extract PlexLibraryDto list from response with auto-tagging List _extractLibraryList(MediaServerResponse response) { final container = _getMediaContainer(response); if (container != null && container['Directory'] != null) { @@ -616,7 +603,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return []; } - /// Extract PlexPlaylistDto list from response with auto-tagging List _extractPlaylistList(MediaServerResponse response) { final container = _getMediaContainer(response); if (container != null && container['Metadata'] != null) { @@ -631,11 +617,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return []; } - // ============================================================================ - // API Methods - // ============================================================================ - - /// Get server identity Future> getServerIdentity() async { final response = await _getWithFailover('/identity'); return response.data; @@ -981,8 +962,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { 'Failed to select streams', ); } - // Si allParts est false, retourner true ou false explicitement (selon la logique souhaitée) - // Ici, on retourne true par défaut si rien n'est fait return true; } @@ -1256,13 +1235,10 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { if (metadataJson['Media'] != null && (metadataJson['Media'] as List).isNotEmpty) { final mediaList = metadataJson['Media'] as List; - // Parse available media versions first (convert via the internal - // mapper so PlaybackInitializationResult sees neutral MediaVersion). availableVersions = mediaList .map((media) => PlexMappers.mediaVersionFromJson(media as Map)) .toList(); - // Ensure the requested index is valid if (mediaIndex < 0 || mediaIndex >= mediaList.length) { mediaIndex = 0; } @@ -1281,15 +1257,11 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { final partKey = part['key'] as String?; if (partKey != null) { - // Get video URL videoUrl = '${config.baseUrl}$partKey'.withPlexToken(config.token); - // Parse streams using shared parser final streams = walkStreams(part['Stream'] as List?, _streamReader); - // Parse chapters using helper final chapters = _parseChapters(metadataJson); - // Create media info mediaInfo = MediaSourceInfo( videoUrl: videoUrl, audioTracks: streams.audioTracks, @@ -1941,10 +1913,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { ); } - // ============================================================================ - // Metadata Editing Methods - // ============================================================================ - /// Update metadata fields for a media item Future updateMetadata({ required int sectionId, @@ -2099,10 +2067,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { ); } - // ============================================================================ - // Collection Methods - // ============================================================================ - /// Get all collections for a library section /// Returns collections as PlexMetadataDto objects with type="collection" Future> _getLibraryCollections(String sectionId) async { @@ -2255,10 +2219,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return result; } - // ============================================================================ - // Play Queue Methods - // ============================================================================ - /// Parse a `/playQueues/{id}` response into a [PlayQueueResponse] with /// MediaItem-typed entries. PlayQueueResponse _parsePlayQueueResponse(dynamic data) { @@ -2496,10 +2456,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { return _getPlaylists(playlistType: playlistType); } - // ============================================================================ - // Library Management Methods - // ============================================================================ - /// Scan/refresh a library section to detect new files Future scanLibrary(String sectionId) async { await _getWithFailover('/library/sections/$sectionId/refresh'); @@ -2521,10 +2477,6 @@ class PlexClient with MediaServerCacheMixin implements MediaServerClient { await _getWithFailover('/library/sections/$sectionId/analyze'); } - // ============================================================================ - // Live TV / DVR Methods - // ============================================================================ - /// Get all DVR devices configured on this server Future> getDvrs() async { return _wrapListApiCall(() => _http.get('/livetv/dvrs'), (response) { diff --git a/lib/services/settings_export_service.dart b/lib/services/settings_export_service.dart index c7ef63e6..60902fe9 100644 --- a/lib/services/settings_export_service.dart +++ b/lib/services/settings_export_service.dart @@ -269,8 +269,6 @@ class SettingsExportService { return false; } - // --- File operations ----------------------------------------------------- - static Future _defaultFileName() async { final now = DateTime.now(); final y = now.year.toString().padLeft(4, '0'); diff --git a/lib/services/track_selection_service.dart b/lib/services/track_selection_service.dart index 01230e95..01e34e5a 100644 --- a/lib/services/track_selection_service.dart +++ b/lib/services/track_selection_service.dart @@ -10,9 +10,6 @@ import '../utils/future_extensions.dart'; import '../utils/app_logger.dart'; import '../utils/language_codes.dart'; -// ============================================================================ -// Track Matching Utilities -// ============================================================================ // These functions match MPV tracks to Plex tracks by properties (language, // codec, title, etc.) instead of list index, since the two may be ordered // differently. diff --git a/lib/utils/media_server_http_client.dart b/lib/utils/media_server_http_client.dart index 8ffa264f..83a82bb1 100644 --- a/lib/utils/media_server_http_client.dart +++ b/lib/utils/media_server_http_client.dart @@ -82,10 +82,6 @@ class MediaServerHttpClient { Duration connectTimeout; Duration receiveTimeout; - // --------------------------------------------------------------------------- - // Public request methods - // --------------------------------------------------------------------------- - Future get( String path, { Map? queryParameters, @@ -190,10 +186,6 @@ class MediaServerHttpClient { void close() => _client.close(); - // --------------------------------------------------------------------------- - // Core send implementation - // --------------------------------------------------------------------------- - Future _send( String method, String path, { @@ -260,10 +252,6 @@ class MediaServerHttpClient { } } - // --------------------------------------------------------------------------- - // URI building - // --------------------------------------------------------------------------- - /// Build a full URI from [baseUrl] + [path] + [queryParameters]. /// Use this from callers that need to construct URLs with the client's /// current (possibly failover-switched) base, rather than reading @@ -307,10 +295,6 @@ class MediaServerHttpClient { static bool _isAbsoluteUrl(String url) => url.startsWith('http://') || url.startsWith('https://'); - // --------------------------------------------------------------------------- - // Body serialization - // --------------------------------------------------------------------------- - /// Set the request body, choosing encoding based on the body type. void _setBody(http.Request request, Object? body) { if (body == null) return; @@ -338,10 +322,6 @@ class MediaServerHttpClient { } } - // --------------------------------------------------------------------------- - // Response decoding - // --------------------------------------------------------------------------- - /// Decode the response body: lenient UTF-8, then JSON parse if applicable. /// Large payloads are decoded in a background isolate. Future _decodeBody(List bytes, Map headers) async { @@ -375,10 +355,6 @@ class MediaServerHttpClient { return null; } - // --------------------------------------------------------------------------- - // Logging - // --------------------------------------------------------------------------- - void _logResponse(String method, Uri uri, int statusCode, int ms) { appLogger.d('$method ${LogRedactionManager.redact(uri.toString())} → $statusCode (${ms}ms)'); }