diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index 34321d89..b1ae3779 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -38,7 +38,6 @@ enum OfflineActionType { }; } -// Simplified database with API cache for offline support @DriftDatabase( tables: [ DownloadedMedia, @@ -556,7 +555,6 @@ LazyDatabase _openConnection() { final file = File(p.join(dbFolder.path, 'plezy_downloads.db')); - // Ensure directory exists if (!await file.parent.exists()) { await file.parent.create(recursive: true); } diff --git a/lib/database/download_operations.dart b/lib/database/download_operations.dart index 53a40d93..82c957b0 100644 --- a/lib/database/download_operations.dart +++ b/lib/database/download_operations.dart @@ -123,7 +123,6 @@ extension DownloadDatabaseOperations on AppDatabase { /// Get next item from queue (highest priority, oldest first) /// Only returns items that are not paused Future getNextQueueItem() async { - // Join with downloadedMedia to check status and filter out paused items final query = select( downloadQueue, ).join([innerJoin(downloadedMedia, downloadedMedia.globalKey.equalsExp(downloadQueue.mediaGlobalKey))]); diff --git a/lib/database/tables.dart b/lib/database/tables.dart index 5cf57b98..ca9ca599 100644 --- a/lib/database/tables.dart +++ b/lib/database/tables.dart @@ -7,7 +7,6 @@ class ApiCache extends Table { /// for Plex, "abc123:/Users/.../Items/..." for Jellyfin) TextColumn get cacheKey => text()(); - /// JSON response data TextColumn get data => text()(); /// Whether this item is pinned for offline access diff --git a/lib/focus/dpad_navigator.dart b/lib/focus/dpad_navigator.dart index 3211da34..c254d056 100644 --- a/lib/focus/dpad_navigator.dart +++ b/lib/focus/dpad_navigator.dart @@ -1,13 +1,9 @@ import 'package:flutter/services.dart'; -/// Extension on KeyEvent for common event type checks. extension KeyEventActionable on KeyEvent { - /// Whether this event should trigger an action (KeyDownEvent or KeyRepeatEvent). - /// Use this to filter out KeyUpEvents early in key handlers. bool get isActionable => this is KeyDownEvent || this is KeyRepeatEvent; } -/// Shared sets for keyboard key categories. final _dpadDirectionKeys = { LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, @@ -31,35 +27,18 @@ final _backKeys = { final _contextMenuKeys = {LogicalKeyboardKey.contextMenu, LogicalKeyboardKey.gameButtonX}; -/// Extension methods for checking D-pad related keys. extension DpadKeyExtension on LogicalKeyboardKey { - /// Whether this key is a D-pad directional key. bool get isDpadDirection => _dpadDirectionKeys.contains(this); - - /// Whether this key is a select/activate key. bool get isSelectKey => _selectKeys.contains(this); - - /// Whether this key is a back/cancel key. bool get isBackKey => _backKeys.contains(this); - - /// Whether this key is a context menu key. bool get isContextMenuKey => _contextMenuKeys.contains(this); - /// Whether this key is a navigation key (dpad, select, back, context menu, tab). - /// Use this to distinguish navigation keys from typing/volume/media keys. bool get isNavigationKey => isDpadDirection || isSelectKey || isBackKey || isContextMenuKey || this == LogicalKeyboardKey.tab; - /// Whether this key moves focus left. bool get isLeftKey => this == LogicalKeyboardKey.arrowLeft; - - /// Whether this key moves focus right. bool get isRightKey => this == LogicalKeyboardKey.arrowRight; - - /// Whether this key moves focus up. bool get isUpKey => this == LogicalKeyboardKey.arrowUp; - - /// Whether this key moves focus down. bool get isDownKey => this == LogicalKeyboardKey.arrowDown; } diff --git a/lib/focus/focus_theme.dart b/lib/focus/focus_theme.dart index 3e7054e7..8b9537c6 100644 --- a/lib/focus/focus_theme.dart +++ b/lib/focus/focus_theme.dart @@ -1,30 +1,21 @@ import 'package:flutter/material.dart'; import '../theme/mono_tokens.dart'; -/// Focus styling constants for D-pad navigation. class FocusTheme { FocusTheme._(); - /// Scale factor when an item is focused. static const double focusScale = 1.02; - - /// Border width for the focus indicator. static const double focusBorderWidth = 2.5; - - /// Default border radius (matches MonoTokens.radiusSm). static const double defaultBorderRadius = 8.0; - /// Get the focus border color from the theme. static Color getFocusBorderColor(BuildContext context) { return Theme.of(context).colorScheme.primary; } - /// Get the animation duration from MonoTokens. static Duration getAnimationDuration(BuildContext context) { return Theme.of(context).extension()?.fast ?? const Duration(milliseconds: 150); } - /// Build the focus border decoration. static BoxDecoration focusDecoration( BuildContext context, { required bool isFocused, diff --git a/lib/focus/focusable_action_bar.dart b/lib/focus/focusable_action_bar.dart index f926c2c3..52d01e9f 100644 --- a/lib/focus/focusable_action_bar.dart +++ b/lib/focus/focusable_action_bar.dart @@ -5,22 +5,13 @@ import 'focus_theme.dart'; import 'input_mode_tracker.dart'; import 'key_event_utils.dart'; -/// Describes a single action button for use in [FocusableActionBar]. class FocusableAction { - /// Icon to display. Ignored when [child] is provided. final IconData icon; - - /// Icon color. Ignored when [child] is provided. final Color? iconColor; - - /// Icon fill weight (0.0–1.0). Defaults to 1.0. Ignored when [child] is provided. final double iconFill; final String? tooltip; final VoidCallback? onPressed; - - /// Optional custom child widget placed inside the focus container. - /// Overrides the default [IconButton] built from [icon]/[tooltip]/[onPressed]. final Widget? child; const FocusableAction({ @@ -33,25 +24,6 @@ class FocusableAction { }); } -/// A row of focusable action buttons for app bar [actions:]. -/// -/// Manages focus nodes, left/right D-pad navigation between buttons, -/// and the standard white-alpha background focus indicator internally. -/// -/// Returns a single [Row] widget — place it inside the `actions:` list: -/// ```dart -/// CustomAppBar( -/// title: Text('Title'), -/// actions: [ -/// FocusableActionBar( -/// actions: [ -/// FocusableAction(icon: Symbols.refresh_rounded, onPressed: _refresh), -/// FocusableAction(icon: Symbols.upload_rounded, onPressed: _upload), -/// ], -/// ), -/// ], -/// ) -/// ``` class FocusableActionBar extends StatefulWidget { final List actions; diff --git a/lib/focus/focusable_button.dart b/lib/focus/focusable_button.dart index e281d423..fae439e3 100644 --- a/lib/focus/focusable_button.dart +++ b/lib/focus/focusable_button.dart @@ -4,23 +4,6 @@ import 'focus_theme.dart'; import 'focusable_wrapper.dart'; import 'input_mode_tracker.dart'; -/// A focusable button wrapper for D-pad navigation on TV. -/// -/// Wraps any button widget with [FocusableWrapper] and adds a white overlay -/// + contrasting border when focused. Tracks focus state internally so callers -/// don't need manual state management. -/// -/// ```dart -/// FocusableButton( -/// autofocus: true, -/// onPressed: _doSomething, -/// child: FilledButton.icon( -/// onPressed: _doSomething, -/// icon: Icon(Symbols.add_rounded), -/// label: Text('Create'), -/// ), -/// ) -/// ``` class FocusableButton extends StatefulWidget { final Widget child; final VoidCallback? onPressed; diff --git a/lib/focus/focusable_chip_mixin.dart b/lib/focus/focusable_chip_mixin.dart index bff3f91b..dd6f924a 100644 --- a/lib/focus/focusable_chip_mixin.dart +++ b/lib/focus/focusable_chip_mixin.dart @@ -6,27 +6,13 @@ import 'package:flutter/services.dart'; import 'dpad_navigator.dart'; import 'key_event_utils.dart'; -/// Callbacks for chip key event handling. class ChipKeyCallbacks { - /// Called when SELECT key is pressed (short press when [onLongPress] is set). final VoidCallback? onSelect; - - /// Called when SELECT key is held for 500ms. final VoidCallback? onLongPress; - - /// Called when DOWN arrow is pressed. final VoidCallback? onNavigateDown; - - /// Called when UP arrow is pressed. final VoidCallback? onNavigateUp; - - /// Called when LEFT arrow is pressed. final VoidCallback? onNavigateLeft; - - /// Called when RIGHT arrow is pressed. final VoidCallback? onNavigateRight; - - /// Called when BACK key is pressed. final VoidCallback? onBack; const ChipKeyCallbacks({ @@ -168,7 +154,6 @@ mixin FocusableChipStateMixin on State { return KeyEventResult.ignored; } - // LEFT arrow - call callback if provided, otherwise propagate to parent if (key.isLeftKey) { if (callbacks.onNavigateLeft != null) { callbacks.onNavigateLeft!(); @@ -178,7 +163,6 @@ mixin FocusableChipStateMixin on State { return KeyEventResult.ignored; } - // RIGHT arrow - call callback if provided, otherwise consume to prevent escape if (key.isRightKey) { if (callbacks.onNavigateRight != null) { callbacks.onNavigateRight!(); @@ -187,13 +171,11 @@ mixin FocusableChipStateMixin on State { return KeyEventResult.handled; } - // DOWN arrow if (key.isDownKey) { callbacks.onNavigateDown?.call(); return KeyEventResult.handled; } - // UP arrow if (key.isUpKey && callbacks.onNavigateUp != null) { callbacks.onNavigateUp!(); return KeyEventResult.handled; diff --git a/lib/focus/focusable_slider.dart b/lib/focus/focusable_slider.dart index 5cd641fa..4104299c 100644 --- a/lib/focus/focusable_slider.dart +++ b/lib/focus/focusable_slider.dart @@ -4,11 +4,6 @@ import 'dpad_navigator.dart'; import 'focusable_wrapper.dart'; import 'input_mode_tracker.dart'; -/// A D-pad friendly slider that uses LEFT/RIGHT to adjust value -/// and lets UP/DOWN pass through for focus traversal. -/// -/// Hides the thumb in keyboard mode when not focused, matching -/// the timeline slider pattern. class FocusableSlider extends StatefulWidget { final double value; final double min; diff --git a/lib/focus/key_event_utils.dart b/lib/focus/key_event_utils.dart index 0dd64e1e..bccca113 100644 --- a/lib/focus/key_event_utils.dart +++ b/lib/focus/key_event_utils.dart @@ -52,10 +52,7 @@ class BackKeyCoordinator { } } -/// Handle a BACK key press by running [onBack] on key up. -/// -/// This consumes KeyDown/KeyRepeat to avoid duplicate actions from key repeat. -/// Optionally suppresses stray KeyUp events delivered to the next route after a pop. +/// Consumes KeyDown/KeyRepeat to avoid duplicate actions, runs [onBack] on KeyUp. KeyEventResult handleBackKeyAction(KeyEvent event, VoidCallback onBack) { if (!event.logicalKey.isBackKey) return KeyEventResult.ignored; @@ -92,12 +89,8 @@ KeyEventResult handleBackKeyNavigation(BuildContext context, KeyEvent event, return handleBackKeyAction(event, () => Navigator.pop(context, result)); } -/// Handles a select key as a one-shot button activation. -/// -/// Fires [onActivate] on the initial [KeyDownEvent] only. -/// Consumes all select key events (down, repeat, up) to prevent -/// unhandled events from reaching platform-level handling. -/// Returns [KeyEventResult.ignored] for non-select keys. +/// Consumes all select-key events (down, repeat, up) so they don't reach +/// platform-level handlers; fires [onActivate] on the initial KeyDown only. KeyEventResult handleOneShotSelect(KeyEvent event, VoidCallback onActivate) { if (!event.logicalKey.isSelectKey) return KeyEventResult.ignored; if (event is KeyDownEvent) onActivate(); diff --git a/lib/focus/key_repeat_helper.dart b/lib/focus/key_repeat_helper.dart index a5a2cb45..c6b318ae 100644 --- a/lib/focus/key_repeat_helper.dart +++ b/lib/focus/key_repeat_helper.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -/// Key-repeat timer for held dpad/keyboard inputs: fires immediately, then -/// every 100 ms after a 400 ms initial delay. Call [stopRepeat] in `dispose`. mixin KeyRepeatHelper on State { static const _initialDelay = Duration(milliseconds: 400); static const _repeatInterval = Duration(milliseconds: 100); diff --git a/lib/focus/locked_hub_controller.dart b/lib/focus/locked_hub_controller.dart index 1fca807a..6c13aebb 100644 --- a/lib/focus/locked_hub_controller.dart +++ b/lib/focus/locked_hub_controller.dart @@ -8,7 +8,6 @@ class HubFocusMemory { static final Map _perHubMemory = {}; static int _lastColumnHint = 0; - /// Remember the focused index for a specific hub static void setForHub(String hubKey, int index) { _perHubMemory[hubKey] = index; _lastColumnHint = index; diff --git a/lib/main.dart b/lib/main.dart index f539562f..594ca3fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,7 +150,6 @@ Future _bootstrapApp() async { unawaited(LocaleSettings.setLocale(savedLocale)); - // Needed for formatting dates in different locales await initializeDateFormatting(savedLocale.languageCode, null); // Configure image cache — keep budget modest to leave headroom for Skia decode buffers @@ -176,7 +175,6 @@ Future _bootstrapApp() async { PipService(); } - // Configure macOS window with custom titlebar (depends on window manager) futures.add(MacOSWindowService.setupCustomTitlebar()); // Hook Windows native fullscreen callback (no-op elsewhere). @@ -216,15 +214,12 @@ Future _bootstrapApp() async { // and intercepts input events, so we must listen to re-dispatch them) GamepadService.instance.start(); - // Desktop-only services if (PlatformDetector.isDesktopOS()) { unawaited(DiscordRPCService.instance.initialize()); } - // Trakt scrobble service (all platforms) await TraktScrobbleService.instance.initialize(); - // Register bundled shader licenses _registerShaderLicenses(); // In release mode, show a colored placeholder instead of a blank/white screen @@ -380,7 +375,6 @@ void _registerShaderLicenses() { }); } -// Global RouteObserver for tracking navigation final RouteObserver routeObserver = RouteObserver(); final rootNavigatorKey = GlobalKey(); @@ -403,7 +397,6 @@ class MainApp extends StatefulWidget { } class _MainAppState extends State with WidgetsBindingObserver { - // Initialize multi-server infrastructure late final MultiServerManager _serverManager; late final DataAggregationService _aggregationService; late final AppDatabase _appDatabase; @@ -428,7 +421,6 @@ class _MainAppState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); - // On desktop, periodically check RSS and evict image cache if too high if (PlatformDetector.isDesktopOS()) { _memoryCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) { final rss = ProcessInfo.currentRss; @@ -444,7 +436,6 @@ class _MainAppState extends State with WidgetsBindingObserver { _aggregationService = DataAggregationService(_serverManager); _appDatabase = AppDatabase(); - // Initialize API cache with database PlexApiCache.initialize(_appDatabase); JellyfinApiCache.initialize(_appDatabase); @@ -691,7 +682,6 @@ class _MainAppState extends State with WidgetsBindingObserver { }, dispose: (_, binder) => binder.dispose(), ), - // Offline mode provider - depends on MultiServerProvider ChangeNotifierProxyProvider( create: (_) { final provider = OfflineModeProvider(_serverManager); @@ -714,7 +704,6 @@ class _MainAppState extends State with WidgetsBindingObserver { return provider; }, ), - // Offline watch sync service ChangeNotifierProxyProvider( create: (context) { final offlineModeProvider = context.read(); @@ -764,7 +753,6 @@ class _MainAppState extends State with WidgetsBindingObserver { return provider; }, ), - // Offline watch provider - depends on sync service and download provider ChangeNotifierProxyProvider2( create: (context) => OfflineWatchProvider( syncService: _offlineWatchSyncService, @@ -1097,7 +1085,6 @@ class _SetupScreenState extends State { return; } - // Populate per-server status from the registry for the splash list. if (mounted) { setState(() { for (final conn in allConnections) { diff --git a/lib/media/live_tv_support.dart b/lib/media/live_tv_support.dart index 381ac88c..188025d3 100644 --- a/lib/media/live_tv_support.dart +++ b/lib/media/live_tv_support.dart @@ -96,7 +96,6 @@ abstract class LiveTvSupport { /// locally. Future setFavoriteChannels(List channels); - // ── DVR setup/lifecycle ───────────────────────────────────────── Future fetchLiveTvServerStatus(); Future fetchDvr(String dvrId); Future> createDvr({ @@ -115,7 +114,6 @@ abstract class LiveTvSupport { Future> reloadGuide(String dvrId); Future cancelGuideReload(String dvrId); - // ── Grabber devices/tuners ────────────────────────────────────── Future> fetchGrabbers({String? protocol}); Future> fetchGrabberDevices(); Future>> discoverGrabberDevices(); @@ -136,7 +134,6 @@ abstract class LiveTvSupport { Future updateGrabberDevicePrefs(String deviceId, Map prefs); String buildGrabberDeviceThumbUrl(String deviceId, int version); - // ── EPG setup/lineups ─────────────────────────────────────────── Future> fetchEpgCountries(); Future> fetchEpgLanguages(); Future> fetchEpgRegions(String country, String epgId); @@ -149,7 +146,6 @@ abstract class LiveTvSupport { required String lineupGroupUri, }); - // ── Recording rules / scheduled grabs ─────────────────────────── Future> getSubscriptionTemplate(String guid); Future> fetchRecordingRules({bool includeGrabs = true, bool includeStorage = true}); Future fetchRecordingRule( @@ -170,7 +166,6 @@ abstract class LiveTvSupport { bool includeStorage = true, }); - // ── Providers and sessions ────────────────────────────────────── Future> fetchMediaProviders(); Future registerMediaProvider(String url); Future refreshMediaProviders(); diff --git a/lib/media/media_item.dart b/lib/media/media_item.dart index 632aabe0..9e4cfd7d 100644 --- a/lib/media/media_item.dart +++ b/lib/media/media_item.dart @@ -28,7 +28,6 @@ sealed class MediaItem { /// for matching across servers and for Trakt-style external lookups. final String? guid; - // ── Display metadata ───────────────────────────────────────────── final String? title; final String? titleSort; final String? summary; @@ -41,7 +40,6 @@ sealed class MediaItem { final String? originallyAvailableAt; final String? contentRating; - // ── Hierarchy (episodes/seasons) ───────────────────────────────── final String? parentId; final String? parentTitle; final String? parentThumbPath; @@ -52,13 +50,11 @@ sealed class MediaItem { final String? grandparentThumbPath; final String? grandparentArtPath; - // ── Artwork ────────────────────────────────────────────────────── final String? thumbPath; final String? artPath; final String? clearLogoPath; final String? backgroundSquarePath; - // ── Time / watch state ────────────────────────────────────────── final int? durationMs; /// Resume position in ms. @@ -78,11 +74,9 @@ sealed class MediaItem { final int? addedAt; final int? updatedAt; - // ── Rating ─────────────────────────────────────────────────────── final double? rating; final double? userRating; - // ── Tags / people ──────────────────────────────────────────────── final List? genres; final List? directors; final List? writers; @@ -94,15 +88,12 @@ sealed class MediaItem { final List? moods; final List? roles; - // ── Media files ────────────────────────────────────────────────── final List? mediaVersions; - // ── Library reference ──────────────────────────────────────────── /// Backend-opaque library/section id this item belongs to. final String? libraryId; final String? libraryTitle; - // ── Per-item playback prefs ────────────────────────────────────── /// Preferred audio language for this item — used by track-selection /// fallback (Priority 3) on both backends. Plex persists changes via /// [PlexClient.setMetadataPreferences]; Jellyfin populates it from the @@ -110,11 +101,9 @@ sealed class MediaItem { /// endpoint, so the value is read-only there. final String? audioLanguage; - // ── Multi-server ───────────────────────────────────────────────── final String? serverId; final String? serverName; - // ── Escape hatch ───────────────────────────────────────────────── /// Untyped fall-through for backend-specific fields not yet mapped onto a /// typed accessor. Use sparingly; promote to typed fields when stable. final Map? raw; diff --git a/lib/media/media_server_client.dart b/lib/media/media_server_client.dart index 8b5b9ea8..f3993e22 100644 --- a/lib/media/media_server_client.dart +++ b/lib/media/media_server_client.dart @@ -62,13 +62,11 @@ import 'server_capabilities.dart'; enum HealthStatus { online, offline, authError } abstract class MediaServerClient { - // ── Identity ───────────────────────────────────────────────────── String get serverId; String? get serverName; MediaBackend get backend; ServerCapabilities get capabilities; - // ── Lifecycle ──────────────────────────────────────────────────── /// Release HTTP resources and any other long-lived state. Idempotent. void close(); @@ -96,7 +94,6 @@ abstract class MediaServerClient { /// appropriate cache instance. ApiCache get cache; - // ── Browse: libraries ──────────────────────────────────────────── Future> fetchLibraries(); /// Page through items in [libraryId] using the neutral [query]. Backends @@ -151,7 +148,6 @@ abstract class MediaServerClient { /// children). Future refreshLibraryMetadata(String libraryId); - // ── Browse: items ──────────────────────────────────────────────── /// Fetch a single item by its backend-opaque id. Returns `null` when the /// item no longer exists or the user can't see it. Future fetchItem(String id); @@ -204,7 +200,6 @@ abstract class MediaServerClient { /// internally; the neutral name matches the Continue Watching UI surface. Future> fetchContinueWatching({int count = 20}); - // ── Browse: hubs ───────────────────────────────────────────────── /// Curated home-screen hubs across all libraries (Plex Discover; Jellyfin /// synthesizes `Latest` + `Resume` + `NextUp`). Future> fetchGlobalHubs({int limit = 10}); @@ -223,7 +218,6 @@ abstract class MediaServerClient { /// (Latest / Resume / NextUp) without the preview limit. Future> fetchMoreHubItems(String hubId, {int? limit}); - // ── Watch state ────────────────────────────────────────────────── /// Mark [item] as watched. The full item is passed (not just an id) so /// implementations can fire a [WatchStateEvent] on [WatchStateNotifier] /// for UI invalidation — episode/season/show parent chain, library @@ -242,7 +236,6 @@ abstract class MediaServerClient { /// awaited call in `try/catch` and surface a snackbar on the catch arm. Future rate(MediaItem item, double rating); - // ── Playlists ──────────────────────────────────────────────────── Future> fetchPlaylists({String playlistType = 'video', bool? smart}); /// Metadata only — items are fetched via [fetchPlaylistItems]. @@ -282,7 +275,6 @@ abstract class MediaServerClient { /// the same caveats about backend tagging and the per-playlist id. Future removeFromPlaylist({required String playlistId, required MediaItem item}); - // ── Collections ────────────────────────────────────────────────── /// Collections in [libraryId]. Plex hits `/library/sections/{id}/collections`; /// Jellyfin queries `/Items?ParentId={libraryId}&IncludeItemTypes=BoxSet`. /// Each result carries `kind == MediaKind.collection`. @@ -322,7 +314,6 @@ abstract class MediaServerClient { /// read [MediaItem.libraryId] for backends that need it (Plex). Future deleteCollection(MediaItem collection); - // ── Item write ─────────────────────────────────────────────────── /// Permanently delete [item] from the library. Future deleteMediaItem(MediaItem item); @@ -332,7 +323,6 @@ abstract class MediaServerClient { /// the server has no info to show. Future getFileInfo(MediaItem item); - // ── Images ─────────────────────────────────────────────────────── /// Resolve a backend-relative thumbnail path to a fully-qualified URL ready /// for cached image providers. Returns an empty string for null/empty /// inputs. @@ -355,14 +345,12 @@ abstract class MediaServerClient { /// engine alongside the URL. Map get streamHeaders; - // ── External IDs ───────────────────────────────────────────────── /// External IDs (IMDb / TMDB / TVDB) for [itemId]. Plex hits /// `/library/metadata/{id}?includeGuids=1`; Jellyfin reads the inline /// `ProviderIds` map. Returns an empty [ExternalIds] when the server /// has no external mapping for the item. Future fetchExternalIds(String itemId); - // ── Hubs: extras ───────────────────────────────────────────────── /// Chapters and intro/credits markers for [itemId]. Plex returns both in one /// round trip; Jellyfin combines item-level chapters with best-effort native /// media segments. Implementations may cache. @@ -401,7 +389,6 @@ abstract class MediaServerClient { /// `trickplayByWidth` map). Future createScrubPreviewSource({required MediaItem item, required MediaSourceInfo mediaSource}); - // ── Playback progress ──────────────────────────────────────────── /// Watched threshold (0.0–1.0). An item is considered "watched" when /// `position / duration` crosses this value. Plex reads it from the /// server's `LibraryVideoPlayedThreshold` pref; Jellyfin doesn't expose @@ -452,7 +439,6 @@ abstract class MediaServerClient { String? mediaSourceId, }); - // ── Playback initialization ────────────────────────────────────── /// Resolve the video URL, media info, and external subtitle list for /// playback. Backends own the per-backend particulars: Plex runs the /// transcode-decision flow when [PlaybackInitializationOptions.qualityPreset] @@ -465,13 +451,11 @@ abstract class MediaServerClient { /// metadata, even when the caller intends to play a downloaded copy. Future getPlaybackInitialization(PlaybackInitializationOptions options); - // ── Live TV ────────────────────────────────────────────────────── /// Backend-neutral live-TV operations. Always returns a wrapper; consult /// [LiveTvSupport.isAvailable] to find out whether the server actually /// has live TV configured before calling other methods. LiveTvSupport get liveTv; - // ── Downloads ──────────────────────────────────────────────────── /// Resolve the download URL for [item]'s primary video file along with /// any external subtitle tracks that should be saved alongside it. /// diff --git a/lib/media/media_sort.dart b/lib/media/media_sort.dart index 0fe56d95..168c9d8c 100644 --- a/lib/media/media_sort.dart +++ b/lib/media/media_sort.dart @@ -15,19 +15,14 @@ class MediaSort { Map toJson() => _$MediaSortToJson(this); - /// Gets the full sort key with direction - /// If [descending] is true, returns the descKey or key:desc - /// Otherwise returns the key for ascending sort String getSortKey({bool descending = false}) { if (!descending) { return key; } - // Use descKey if available, otherwise append :desc to key return descKey ?? '$key:desc'; } - /// Returns true if this sort's default direction is descending bool get isDefaultDescending { return defaultDirection?.toLowerCase() == 'desc'; } diff --git a/lib/media/media_source_info.dart b/lib/media/media_source_info.dart index e5bb6141..7f18b722 100644 --- a/lib/media/media_source_info.dart +++ b/lib/media/media_source_info.dart @@ -71,9 +71,6 @@ mixin _TrackLabelMixin { String? get displayTitle; String? get language; - /// Builds a label from the given parts - /// If displayTitle is present, returns it - /// Otherwise, combines language and additional parts String buildLabel(List additionalParts) { if (displayTitle != null && displayTitle!.isNotEmpty) { return displayTitle!; diff --git a/lib/mixins/grid_focus_node_mixin.dart b/lib/mixins/grid_focus_node_mixin.dart index 5bcf6048..ae5e334a 100644 --- a/lib/mixins/grid_focus_node_mixin.dart +++ b/lib/mixins/grid_focus_node_mixin.dart @@ -25,7 +25,6 @@ mixin GridFocusNodeMixin on State { return index == 0 ? firstNode : getGridItemFocusNode(index, prefix: prefix); } - /// Record that the item at [index] received focus. void trackGridItemFocus(int index, bool hasFocus) { if (hasFocus) { lastFocusedGridIndex = index; @@ -37,7 +36,6 @@ mixin GridFocusNodeMixin on State { bool get shouldRestoreGridFocus => lastFocusedGridIndex != null && lastFocusedGridContentVersion == gridContentVersion && lastFocusedGridIndex! >= 0; - /// Remove focus nodes for indices >= [itemCount]. void cleanupGridFocusNodes(int itemCount) { final keysToRemove = gridItemFocusNodes.keys.where((i) => i >= itemCount).toList(); for (final key in keysToRemove) { @@ -68,7 +66,6 @@ mixin GridFocusNodeMixin on State { } } - /// Dispose all grid-item focus nodes. void disposeGridFocusNodes() { for (final node in gridItemFocusNodes.values) { node.dispose(); diff --git a/lib/mixins/library_tab_focus_mixin.dart b/lib/mixins/library_tab_focus_mixin.dart index 341fe3d4..554e161d 100644 --- a/lib/mixins/library_tab_focus_mixin.dart +++ b/lib/mixins/library_tab_focus_mixin.dart @@ -7,10 +7,8 @@ mixin LibraryTabFocusMixin on State { /// Focus node for the first item (for programmatic focus) late final FocusNode firstItemFocusNode; - /// Debug label for the focus node String get focusNodeDebugLabel; - /// Number of items in the list/grid int get itemCount; @override diff --git a/lib/mixins/library_tab_state.dart b/lib/mixins/library_tab_state.dart index 5dde1da2..06c92598 100644 --- a/lib/mixins/library_tab_state.dart +++ b/lib/mixins/library_tab_state.dart @@ -7,7 +7,6 @@ import '../utils/provider_extensions.dart'; /// Mixin providing common functionality for library tab screens /// Provides server-specific client resolution for multi-server support mixin LibraryTabStateMixin on State { - /// The library being displayed MediaLibrary get library; /// Get the [PlexClient] for this library's server. Throws if unavailable. diff --git a/lib/mixins/refreshable.dart b/lib/mixins/refreshable.dart index 9a4056ca..3b97f208 100644 --- a/lib/mixins/refreshable.dart +++ b/lib/mixins/refreshable.dart @@ -2,23 +2,19 @@ mixin Refreshable { void refresh(); } -/// Mixin for screens that support full refresh (clearing all cached data) mixin FullRefreshable { void fullRefresh(); } -/// Mixin for screens with focusable tab content mixin FocusableTab { void focusActiveTabIfReady(); } -/// Mixin for screens with focusable search input mixin SearchInputFocusable { void focusSearchInput(); void setSearchQuery(String query); } -/// Mixin for screens that can load a specific library by key mixin LibraryLoadable { void loadLibraryByKey(String libraryGlobalKey); } diff --git a/lib/mixins/tab_navigation_mixin.dart b/lib/mixins/tab_navigation_mixin.dart index 19e59f71..64d80f73 100644 --- a/lib/mixins/tab_navigation_mixin.dart +++ b/lib/mixins/tab_navigation_mixin.dart @@ -76,7 +76,6 @@ mixin TabNavigationMixin on State, TickerProviderSt FocusNode getTabChipFocusNode(int index) => tabChipFocusNodes[index]; - /// Focus the currently selected tab chip. void focusTabBar() { setState(() { suppressAutoFocus = true; @@ -84,7 +83,6 @@ mixin TabNavigationMixin on State, TickerProviderSt getTabChipFocusNode(tabController.index).requestFocus(); } - /// Navigate back from the tab bar to the sidebar. void onTabBarBack() { MainScreenFocusScope.of(context)?.focusSidebar(); } diff --git a/lib/models/companion_remote/remote_command.dart b/lib/models/companion_remote/remote_command.dart index 09a69268..cb1e8a0e 100644 --- a/lib/models/companion_remote/remote_command.dart +++ b/lib/models/companion_remote/remote_command.dart @@ -1,5 +1,4 @@ enum RemoteCommandType { - // Navigation dpadUp, dpadDown, dpadLeft, @@ -8,7 +7,6 @@ enum RemoteCommandType { back, contextMenu, - // Playback play, pause, playPause, @@ -20,13 +18,11 @@ enum RemoteCommandType { skipIntro, skipCredits, - // Volume volumeUp, volumeDown, volumeMute, volumeSet, - // Tab Navigation tabNext, tabPrevious, tabDiscover, @@ -35,7 +31,6 @@ enum RemoteCommandType { tabDownloads, tabSettings, - // Quick Actions home, search, subtitles, @@ -43,7 +38,6 @@ enum RemoteCommandType { qualitySettings, fullscreen, - // Session Management ping, pong, deviceInfo, diff --git a/lib/models/download_models.dart b/lib/models/download_models.dart index e128e544..90ab449c 100644 --- a/lib/models/download_models.dart +++ b/lib/models/download_models.dart @@ -41,7 +41,6 @@ class DownloadProgress { String get downloadedFormatted => ByteFormatter.formatBytes(downloadedBytes); String get totalFormatted => ByteFormatter.formatBytes(totalBytes); - /// Check if this progress update includes artwork paths bool get hasArtworkPaths => thumbPath != null; DownloadProgress copyWith({ diff --git a/lib/models/livetv_channel.dart b/lib/models/livetv_channel.dart index 1c6220f2..c32f1b1c 100644 --- a/lib/models/livetv_channel.dart +++ b/lib/models/livetv_channel.dart @@ -31,7 +31,6 @@ String favoriteChannelKey(String source, String id) => '$source\u0000$id'; String liveTvChannelScopeKey(LiveTvChannel channel) => '${channel.serverId ?? ''}\u0000${channel.liveDvrKey ?? ''}\u0000${channel.key}'; -/// Represents a Live TV channel from the EPG @JsonSerializable(createToJson: false) class LiveTvChannel with MultiServerFields { @JsonKey(readValue: _readChannelKey) diff --git a/lib/models/livetv_program.dart b/lib/models/livetv_program.dart index 6d45be74..5287b2b0 100644 --- a/lib/models/livetv_program.dart +++ b/lib/models/livetv_program.dart @@ -124,26 +124,21 @@ class LiveTvProgram { ); } - /// Start time as DateTime DateTime? get startTime => beginsAt != null ? DateTime.fromMillisecondsSinceEpoch(beginsAt! * 1000) : null; - /// End time as DateTime DateTime? get endTime => endsAt != null ? DateTime.fromMillisecondsSinceEpoch(endsAt! * 1000) : null; - /// Duration in minutes int get durationMinutes { if (beginsAt == null || endsAt == null) return 0; return ((endsAt! - beginsAt!) / 60).round(); } - /// Whether this program is currently airing bool get isCurrentlyAiring { if (beginsAt == null || endsAt == null) return false; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; return now >= beginsAt! && now < endsAt!; } - /// Progress through the program (0.0 to 1.0) double get progress { if (beginsAt == null || endsAt == null) return 0.0; final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; @@ -152,7 +147,6 @@ class LiveTvProgram { return (now - beginsAt!) / (endsAt! - beginsAt!); } - /// Display title including series info for episodes String get displayTitle { if (grandparentTitle != null && index != null) { final seasonEpisode = parentIndex != null ? 'S${parentIndex}E$index' : 'E$index'; diff --git a/lib/models/mpv_config_models.dart b/lib/models/mpv_config_models.dart index b763b258..b42470a8 100644 --- a/lib/models/mpv_config_models.dart +++ b/lib/models/mpv_config_models.dart @@ -1,4 +1,3 @@ -/// Represents a saved preset of MPV configurations class MpvPreset { final String name; final String text; diff --git a/lib/models/plex/play_queue_response.dart b/lib/models/plex/play_queue_response.dart index b669d749..53c299a2 100644 --- a/lib/models/plex/play_queue_response.dart +++ b/lib/models/plex/play_queue_response.dart @@ -38,7 +38,6 @@ class PlayQueueResponse { } } - /// Get the index of the selected item in the current window int? get selectedItemIndex { if (items == null || playQueueSelectedItemID == null) return null; return items!.indexWhere((item) => item is PlexMediaItem && item.playQueueItemId == playQueueSelectedItemID); diff --git a/lib/models/plex/plex_video_playback_data.dart b/lib/models/plex/plex_video_playback_data.dart index 501b0866..24fc7742 100644 --- a/lib/models/plex/plex_video_playback_data.dart +++ b/lib/models/plex/plex_video_playback_data.dart @@ -4,16 +4,12 @@ import '../../media/media_version.dart'; /// Consolidated data model containing all information needed for video playback. /// This model combines data from multiple Plex API endpoints to reduce redundant requests. class PlexVideoPlaybackData { - /// Direct video URL for playback final String? videoUrl; - /// Media information including audio/subtitle tracks and chapters final MediaSourceInfo? mediaInfo; - /// Available media versions/qualities for this content final List availableVersions; - /// Markers for intro/credits skip functionality final List markers; PlexVideoPlaybackData({ @@ -23,9 +19,7 @@ class PlexVideoPlaybackData { this.markers = const [], }); - /// Returns true if this playback data has a valid video URL bool get hasValidVideoUrl => videoUrl != null && videoUrl!.isNotEmpty; - /// Returns true if media info is available bool get hasMediaInfo => mediaInfo != null; } diff --git a/lib/models/shader_preset.dart b/lib/models/shader_preset.dart index 8d018ecf..5bb0b4a0 100644 --- a/lib/models/shader_preset.dart +++ b/lib/models/shader_preset.dart @@ -1,4 +1,3 @@ -/// Shader preset types available in the app enum ShaderPresetType { none, nvscaler, artcnn, anime4k, custom } /// ArtCNN real-time model sizes. @@ -52,7 +51,6 @@ enum Anime4KMode { modeCA, } -/// Configuration for Anime4K preset class Anime4KConfig { final Anime4KQuality quality; final Anime4KMode mode; @@ -78,7 +76,6 @@ class Anime4KConfig { } } -/// Configuration for ArtCNN preset class ArtCNNConfig { final ArtCNNModel model; final ArtCNNVariant variant; @@ -104,7 +101,6 @@ class ArtCNNConfig { } } -/// Configuration for NVScaler preset class NVScalerConfig { /// Whether to automatically skip NVScaler on HDR content final bool autoHdrSkip; @@ -127,7 +123,6 @@ class NVScalerConfig { } } -/// Represents a shader preset configuration class ShaderPreset { final String id; final String name; @@ -235,7 +230,6 @@ class ShaderPreset { } } - /// Get display name for the mode String get modeDisplayName { if (anime4kConfig != null) { return _getModeName(anime4kConfig!.mode); @@ -243,7 +237,6 @@ class ShaderPreset { return ''; } - /// Get display name for the ArtCNN model String get artcnnModelDisplayName { if (artcnnConfig != null) { return _getArtCNNModelName(artcnnConfig!.model); @@ -251,26 +244,22 @@ class ShaderPreset { return ''; } - /// Get all available preset options static List get allPresets { return [ none, nvscalerDefault, - // ArtCNN presets artcnnPreset(ArtCNNModel.c4f16, ArtCNNVariant.neutral), artcnnPreset(ArtCNNModel.c4f16, ArtCNNVariant.denoise), artcnnPreset(ArtCNNModel.c4f16, ArtCNNVariant.denoiseSharpen), artcnnPreset(ArtCNNModel.c4f32, ArtCNNVariant.neutral), artcnnPreset(ArtCNNModel.c4f32, ArtCNNVariant.denoise), artcnnPreset(ArtCNNModel.c4f32, ArtCNNVariant.denoiseSharpen), - // Anime4K Fast presets anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeA), anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeB), anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeC), anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeAA), anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeBB), anime4kPreset(Anime4KQuality.fast, Anime4KMode.modeCA), - // Anime4K HQ presets anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeA), anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeB), anime4kPreset(Anime4KQuality.hq, Anime4KMode.modeC), @@ -280,7 +269,6 @@ class ShaderPreset { ]; } - /// Find a preset by its ID static ShaderPreset? fromId(String id) { try { return allPresets.firstWhere((p) => p.id == id); diff --git a/lib/mpv/font_loader.dart b/lib/mpv/font_loader.dart index 4e360b7c..11f9948f 100644 --- a/lib/mpv/font_loader.dart +++ b/lib/mpv/font_loader.dart @@ -5,10 +5,8 @@ import 'package:path_provider/path_provider.dart'; import '../utils/app_logger.dart'; -/// Utility class for loading font assets for libass subtitle rendering. -/// -/// Extracts font files from Flutter assets to the app's cache directory to ensure -/// comprehensive Unicode coverage (including CJK characters) for subtitles. +/// Extracts font files from Flutter assets to the cache directory for +/// comprehensive Unicode coverage (including CJK characters) for libass subtitles. class SubtitleFontLoader { static const String _fontAssetPath = 'assets/go-noto-current-regular.ttf'; static const String _fontName = 'Go Noto Current-Regular'; @@ -19,26 +17,21 @@ class SubtitleFontLoader { /// instantiation. static Future? _cachedFontDir; - /// Loads the subtitle font from assets to the cache directory. - /// Returns the directory path containing the font file. static Future loadSubtitleFont() { return _cachedFontDir ??= _loadSubtitleFontOnce(); } static Future _loadSubtitleFontOnce() async { try { - // Get the app's cache directory final cacheDir = await getTemporaryDirectory(); final fontDir = Directory(path.join(cacheDir.path, 'subtitle_fonts')); - // Create fonts directory if it doesn't exist if (!await fontDir.exists()) { await fontDir.create(recursive: true); } final fontFile = File(path.join(fontDir.path, 'go-noto-current-regular.ttf')); - // Load font from assets and write to cache if it doesn't exist if (!await fontFile.exists()) { final fontData = await rootBundle.load(_fontAssetPath); await fontFile.writeAsBytes(fontData.buffer.asUint8List()); @@ -46,15 +39,12 @@ class SubtitleFontLoader { return fontDir.path; } catch (e, st) { - // Return null if font loading fails - libass will fall back gracefully appLogger.w('Failed to load subtitle font', error: e, stackTrace: st); return null; } } - /// Returns the font name to be used with libass. static String get fontName => _fontName; - /// Returns the font asset path. static String get fontAssetPath => _fontAssetPath; } diff --git a/lib/mpv/models.dart b/lib/mpv/models.dart index c6196d9b..d0376a56 100644 --- a/lib/mpv/models.dart +++ b/lib/mpv/models.dart @@ -1,15 +1,11 @@ -/// Represents a contiguous buffered range in the demuxer cache. class BufferRange { final Duration start; final Duration end; const BufferRange({required this.start, required this.end}); } -/// A playback error emitted by the player. -/// -/// [cause] is an optional machine-readable tag (e.g. `server-http-500`) set by -/// the native layer when it can classify the failure, letting the UI branch -/// without parsing [message]. +/// [cause] is an optional machine-readable tag (e.g. `server-http-500`), +/// letting the UI branch without parsing [message]. class PlayerError { /// Cause tag for a server-side HTTP 500 — shared-user bandwidth or /// transcoding limit rejection set by the server owner. @@ -23,63 +19,18 @@ class PlayerError { String toString() => message; } -/// Log level for player messages. -enum PlayerLogLevel { - /// No logging. - none, +enum PlayerLogLevel { none, fatal, error, warn, info, verbose, debug, trace } - /// Fatal errors only. - fatal, - - /// Errors. - error, - - /// Warnings. - warn, - - /// Informational messages. - info, - - /// Verbose output. - verbose, - - /// Debug messages. - debug, - - /// Trace-level output (very verbose). - trace, -} - -/// Represents an audio track in the media. class AudioTrack { - /// Unique identifier for the track. final String id; - - /// Human-readable title of the track. final String? title; - - /// Language code (e.g., 'eng', 'jpn'). final String? language; - - /// Audio codec (e.g., 'aac', 'ac3', 'dts'). final String? codec; - - /// Number of audio channels. final int? channels; - - /// Alias for channels (media_kit compatibility). int? get channelsCount => channels; - - /// Sample rate in Hz. final int? sampleRate; - - /// Bitrate in bits per second. final int? bitrate; - - /// Whether this is the default track. final bool isDefault; - - /// Whether this track is forced. final bool isForced; const AudioTrack({ @@ -94,13 +45,10 @@ class AudioTrack { this.isForced = false, }); - /// Auto-select track. static const auto = AudioTrack(id: 'auto', title: 'Auto'); - /// Disable audio. static const off = AudioTrack(id: 'no', title: 'Off'); - /// Returns a display name for the track. String get displayName { if (title != null && title!.isNotEmpty) return title!; if (language != null && language!.isNotEmpty) return language!; @@ -118,30 +66,14 @@ class AudioTrack { int get hashCode => id.hashCode; } -/// Represents a subtitle track in the media. class SubtitleTrack { - /// Unique identifier for the track. final String id; - - /// Human-readable title of the track. final String? title; - - /// Language code (e.g., 'eng', 'jpn'). final String? language; - - /// Subtitle codec/format (e.g., 'subrip', 'ass', 'pgs'). final String? codec; - - /// Whether this is the default track. final bool isDefault; - - /// Whether this track is forced (e.g., for foreign language segments). final bool isForced; - - /// Whether this is an external subtitle file. final bool isExternal; - - /// URI of external subtitle file (if isExternal is true). final String? uri; const SubtitleTrack({ @@ -155,18 +87,14 @@ class SubtitleTrack { this.uri, }); - /// Create a subtitle track from an external URI. factory SubtitleTrack.uri(String uri, {String? title, String? language}) { return SubtitleTrack(id: 'external:$uri', title: title, language: language, isExternal: true, uri: uri); } - /// Auto-select track. static const auto = SubtitleTrack(id: 'auto', title: 'Auto'); - /// Disable subtitles. static const off = SubtitleTrack(id: 'no', title: 'Off'); - /// Returns a display name for the track. String get displayName { if (title != null && title!.isNotEmpty) return title!; if (language != null && language!.isNotEmpty) return language!; @@ -185,17 +113,12 @@ class SubtitleTrack { int get hashCode => id.hashCode; } -/// Container for all available tracks in the media. class Tracks { - /// Available audio tracks. final List audio; - - /// Available subtitle tracks. final List subtitle; const Tracks({this.audio = const [], this.subtitle = const []}); - /// Creates a copy with the given fields replaced. Tracks copyWith({List? audio, List? subtitle}) { return Tracks(audio: audio ?? this.audio, subtitle: subtitle ?? this.subtitle); } @@ -207,15 +130,9 @@ class Tracks { /// Sentinel value used to distinguish "not provided" from "explicitly set to null" in copyWith. const _sentinel = Object(); -/// Represents the currently selected tracks. class TrackSelection { - /// Currently selected audio track. final AudioTrack? audio; - - /// Currently selected subtitle track. final SubtitleTrack? subtitle; - - /// Currently selected secondary subtitle track (mpv secondary-sid). final SubtitleTrack? secondarySubtitle; const TrackSelection({this.audio, this.subtitle, this.secondarySubtitle}); @@ -236,17 +153,12 @@ class TrackSelection { String toString() => 'TrackSelection(audio: $audio, subtitle: $subtitle, secondarySubtitle: $secondarySubtitle)'; } -/// Represents an audio output device. class AudioDevice { - /// Unique identifier for the device. final String name; - - /// Human-readable description of the device. final String description; const AudioDevice({required this.name, this.description = ''}); - /// Default/auto audio device. static const auto = AudioDevice(name: 'auto', description: 'Auto'); @override @@ -260,15 +172,9 @@ class AudioDevice { int get hashCode => name.hashCode; } -/// A log entry from the player. class PlayerLog { - /// The log level of this message. final PlayerLogLevel level; - - /// The prefix/category of the log message (e.g., 'cplayer', 'ffmpeg'). final String prefix; - - /// The log message text. final String text; const PlayerLog({required this.level, required this.prefix, required this.text}); @@ -277,15 +183,9 @@ class PlayerLog { String toString() => '[$prefix] ${level.name}: $text'; } -/// Represents a media source for the player. class Media { - /// The URI of the media (file path, HTTP URL, etc.). final String uri; - - /// Optional HTTP headers for network requests. final Map? headers; - - /// Optional start position for playback. final Duration? start; const Media(this.uri, {this.headers, this.start}); diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index a908b666..19670a5d 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -3,8 +3,7 @@ import 'package:flutter/services.dart'; import '../../models.dart'; import '../player_base.dart'; -/// Android implementation of [Player] using ExoPlayer. -/// Provides hardware-accelerated playback with ASS subtitle support via libass-android. +/// Android implementation using ExoPlayer with ASS subtitle support via libass-android. class PlayerAndroid extends PlayerBase { static const _methodChannel = MethodChannel('com.plezy/exo_player'); static const _eventChannel = EventChannel('com.plezy/exo_player/events'); @@ -12,7 +11,6 @@ class PlayerAndroid extends PlayerBase { int? _bufferSizeBytes; bool _tunnelingEnabled = true; - /// Stored subtitle track ID when subtitles are hidden via sub-visibility. String? _hiddenSubtitleTrackId; @override @@ -32,7 +30,6 @@ class PlayerAndroid extends PlayerBase { @override void handlePlayerEvent(String name, Map? data) { - // Handle Android-specific events if (name == 'backend-switched') { // Native player switched from ExoPlayer to MPV due to unsupported format. // Clear stale ExoPlayer tracks so applyTrackSelectionWhenReady waits for @@ -42,7 +39,6 @@ class PlayerAndroid extends PlayerBase { return; } - // Delegate to base class for common events super.handlePlayerEvent(name, data); } @@ -170,7 +166,6 @@ class PlayerAndroid extends PlayerBase { @override Future setProperty(String name, String value) async { if (disposed) return; - // ExoPlayer doesn't use MPV properties, but we handle common ones switch (name) { case 'pause': if (value == 'yes') { @@ -196,14 +191,12 @@ class PlayerAndroid extends PlayerBase { break; case 'sub-visibility': if (value == 'no') { - // Store current subtitle track and disable final current = state.track.subtitle; if (current != null && current.id != 'no') { _hiddenSubtitleTrackId = current.id; await selectSubtitleTrack(SubtitleTrack.off); } } else { - // Restore previously hidden subtitle track final storedId = _hiddenSubtitleTrackId; if (storedId != null) { _hiddenSubtitleTrackId = null; @@ -218,7 +211,6 @@ class PlayerAndroid extends PlayerBase { } break; default: - // Forward unknown properties to Kotlin for MPV fallback await invoke('setMpvProperty', {'name': name, 'value': value}); } } @@ -226,7 +218,6 @@ class PlayerAndroid extends PlayerBase { @override Future getProperty(String name) async { if (disposed) return null; - // Return state-based values for common properties switch (name) { case 'pause': return state.playing ? 'no' : 'yes'; @@ -244,12 +235,10 @@ class PlayerAndroid extends PlayerBase { final stats = await getStats(); final mode = stats['dvConversionDebugMode']; return mode?.toString().toLowerCase(); - // Video frame rate - query from ExoPlayer stats case 'container-fps': final fpsStats = await getStats(); final fps = fpsStats['videoFps']; return fps?.toString(); - // Video dimensions - query from ExoPlayer stats case 'width': case 'dwidth': final stats = await getStats(); @@ -265,8 +254,6 @@ class PlayerAndroid extends PlayerBase { } } - /// Get all playback stats from ExoPlayer. - /// Returns a map with video/audio codec info, buffer state, and performance metrics. Future> getStats() async { if (disposed) return {}; try { @@ -277,8 +264,7 @@ class PlayerAndroid extends PlayerBase { } } - /// Get the device's large heap size in MB (Android only). - /// Returns 0 if unavailable. + /// Returns the device's large heap size in MB, or 0 if unavailable (Android only). static Future getHeapSize() async { try { final result = await _methodChannel.invokeMethod('getHeapSize'); @@ -288,7 +274,6 @@ class PlayerAndroid extends PlayerBase { } } - /// Get the current player type ('exoplayer' or 'mpv' if fallback is active). Future getPlayerType() async { if (disposed) return 'unknown'; try { @@ -302,7 +287,6 @@ class PlayerAndroid extends PlayerBase { @override Future command(List args) async { if (disposed) return; - // Handle MPV commands by translating to ExoPlayer equivalents if (args.isEmpty) return; switch (args.first) { diff --git a/lib/mpv/player/platform/player_linux.dart b/lib/mpv/player/platform/player_linux.dart index c18c97bd..e5730664 100644 --- a/lib/mpv/player/platform/player_linux.dart +++ b/lib/mpv/player/platform/player_linux.dart @@ -1,9 +1,5 @@ import '../player_native.dart'; -/// Linux implementation of [Player]. -/// -/// Uses libmpv with FlTextureGL — video is rendered to an offscreen FBO +/// Uses libmpv with FlTextureGL — video rendered to an offscreen FBO /// and composited GPU-side via Flutter's Texture widget. -class PlayerLinux extends PlayerNative { - // textureId is set during initialize() via PlayerNative -} +class PlayerLinux extends PlayerNative {} diff --git a/lib/mpv/player/platform/player_windows.dart b/lib/mpv/player/platform/player_windows.dart index eacfc180..ecce98f4 100644 --- a/lib/mpv/player/platform/player_windows.dart +++ b/lib/mpv/player/platform/player_windows.dart @@ -1,14 +1,11 @@ import '../player_native.dart'; import '../video_rect_support.dart'; -/// Windows implementation of [Player]. -/// -/// Uses libmpv via platform channels with native window embedding. -/// The mpv video window is positioned behind the Flutter window, -/// with transparent regions allowing the video to show through. +/// Uses libmpv with native window embedding behind the Flutter window. class PlayerWindows extends PlayerNative with VideoRectSupport { + // Native window embedding, not a Flutter texture. @override - int? get textureId => null; // Uses native window embedding, not Flutter texture + int? get textureId => null; @override Future setVideoRect({ diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index a7066b6a..032d1a72 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -47,22 +47,16 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { int _nextPropId = 0; final Map _propIdToName = {}; - /// Whether the player has been initialized. - /// Subclasses should set this to true after initialization. @protected bool initialized = false; - /// Whether the player has been disposed. @override bool get disposed => _disposed; - /// The method channel for platform communication. MethodChannel get methodChannel; - /// The event channel for receiving platform events. EventChannel get eventChannel; - /// The log prefix for this player (e.g., 'MPV', 'ExoPlayer'). String get logPrefix; PlayerBase() { @@ -100,8 +94,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { ); } - /// Observes a property on the native player and assigns it a compact propId - /// for efficient event channel communication. @protected Future observeProperty(String name, String format) async { final propId = _nextPropId++; @@ -125,8 +117,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } } - /// Handle a property change event from the platform. - /// Subclasses can override this to handle platform-specific properties. void handlePropertyChange(String name, dynamic value) { if (_disposed) return; switch (name) { @@ -338,8 +328,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } } - /// Handle a player event from the platform. - /// Subclasses can override this to handle platform-specific events. void handlePlayerEvent(String name, Map? data) { if (_disposed) return; switch (name) { @@ -384,7 +372,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } } - /// Parse a log level string to [PlayerLogLevel]. PlayerLogLevel parseLogLevel(String level) { return switch (level) { 'fatal' => PlayerLogLevel.fatal, @@ -398,7 +385,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { }; } - /// Parse a track list from the platform into [Tracks] and selected track IDs. ({Tracks tracks, String? selectedAudioId, String? selectedSubtitleId}) parseTrackList(List trackList) { final audioTracks = []; final subtitleTracks = []; @@ -450,7 +436,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { ); } - /// Update the selected audio track. void updateSelectedAudioTrack(dynamic trackId) { final id = trackId?.toString(); AudioTrack? selectedTrack; @@ -463,7 +448,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { trackController.add(_state.track); } - /// Update the selected subtitle track. void updateSelectedSubtitleTrack(dynamic trackId) { final id = trackId?.toString(); SubtitleTrack? selectedTrack; @@ -476,7 +460,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { trackController.add(_state.track); } - /// Update the selected secondary subtitle track. void updateSelectedSecondarySubtitleTrack(dynamic trackId) { final id = trackId?.toString(); SubtitleTrack? selectedTrack; @@ -505,7 +488,6 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { seekableController.add(seekable); } - /// Safe method channel invocation — no-ops if player is disposed. @protected Future invoke(String method, [dynamic args]) async { if (_disposed) return null; diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 63dd6131..f296a1b6 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -87,8 +87,6 @@ class PlayerNative extends PlayerBase { } } - /// 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 { try { return await invoke('openContentFd', {'uri': contentUri}); @@ -108,26 +106,21 @@ class PlayerNative extends PlayerBase { await _ensureInitialized(); setSeekable(false); - // Show the video layer await setVisible(true); - // Set HTTP headers for Plex authentication and profile if (media.headers != null && media.headers!.isNotEmpty) { final headerList = media.headers!.entries.map((e) => '${e.key}: ${e.value}').toList(); await setProperty('http-header-fields', headerList.join(',')); } - // Set start position if provided (must be set before loading file) + // 'start' must be set before loadfile. if (media.start != null && media.start!.inSeconds > 0) { await setProperty('start', media.start!.inSeconds.toString()); } else { - // Reset start position if not resuming await setProperty('start', 'none'); } - // Set pause BEFORE loadfile to prevent decoder from starting immediately. - // This is important for adding external subtitles before playback begins, - // avoiding a race condition that can freeze the video decoder on Android (issue #226). + // Prevents race condition that can freeze the video decoder on Android (issue #226). if (!play) { await setProperty('pause', 'yes'); } diff --git a/lib/mpv/player/player_state.dart b/lib/mpv/player/player_state.dart index 971ef961..1dfc14fb 100644 --- a/lib/mpv/player/player_state.dart +++ b/lib/mpv/player/player_state.dart @@ -1,59 +1,24 @@ import '../models.dart'; /// Immutable snapshot of the current player state. -/// -/// This class provides synchronous access to the player's current state. /// For reactive updates, use [PlayerStreams]. class PlayerState { - /// Whether playback is currently active. final bool playing; - - /// Whether the media has completed playback. final bool completed; - - /// Whether the player is currently buffering. final bool buffering; - - /// Current playback position. final Duration position; - - /// Total duration of the media. final Duration duration; - - /// Whether the current media item can be seeked. final bool seekable; - - /// Amount of media buffered ahead of current position. final Duration buffer; - - /// Current volume level (0.0 to 100.0). final double volume; - - /// Current playback rate (1.0 = normal speed). final double rate; - - /// Available tracks in the media. final Tracks tracks; - - /// Currently selected tracks. final TrackSelection track; - - /// Audio delay/sync offset in seconds. final double audioDelay; - - /// Subtitle delay/sync offset in seconds. final double subtitleDelay; - - /// Whether audio passthrough is currently enabled. final bool audioPassthrough; - - /// Current audio output device. final AudioDevice audioDevice; - - /// Available audio output devices. final List audioDevices; - - /// Seekable buffered ranges from the demuxer cache. final List bufferRanges; const PlayerState({ @@ -76,7 +41,6 @@ class PlayerState { this.bufferRanges = const [], }); - /// Creates a copy with the given fields replaced. PlayerState copyWith({ bool? playing, bool? completed, diff --git a/lib/mpv/player/player_stream_controllers.dart b/lib/mpv/player/player_stream_controllers.dart index 1d775cae..741d79cd 100644 --- a/lib/mpv/player/player_stream_controllers.dart +++ b/lib/mpv/player/player_stream_controllers.dart @@ -3,12 +3,7 @@ import 'dart:async'; import '../models.dart'; import 'player_streams.dart'; -/// Mixin providing stream controllers for player state changes. -/// -/// This mixin contains the 16 stream controllers used by both -/// [PlayerAndroid] and [PlayerNative] implementations. mixin PlayerStreamControllersMixin { - // Stream controllers final playingController = StreamController.broadcast(); final completedController = StreamController.broadcast(); final bufferingController = StreamController.broadcast(); @@ -28,7 +23,6 @@ mixin PlayerStreamControllersMixin { final playbackRestartController = StreamController.broadcast(); final backendSwitchedController = StreamController.broadcast(); - /// Creates a [PlayerStreams] instance from the stream controllers. PlayerStreams createStreams() { return PlayerStreams( playing: playingController.stream, @@ -52,7 +46,6 @@ mixin PlayerStreamControllersMixin { ); } - /// Closes all stream controllers. Future closeStreamControllers() async { await playingController.close(); await completedController.close(); diff --git a/lib/mpv/player/video_rect_support.dart b/lib/mpv/player/video_rect_support.dart index 1fab6574..9e5389be 100644 --- a/lib/mpv/player/video_rect_support.dart +++ b/lib/mpv/player/video_rect_support.dart @@ -1,17 +1,6 @@ import 'player.dart'; -/// Mixin for players that support video rect positioning. -/// -/// Players that render video behind the Flutter view (e.g., using -/// native window embedding or GtkGLArea) implement this mixin to -/// receive layout updates from the [Video] widget. mixin VideoRectSupport on Player { - /// Updates the video rendering area. - /// - /// Called by the [Video] widget when the layout changes. - /// - /// [left], [top], [right], [bottom] define the rect in physical pixels. - /// [devicePixelRatio] is the device's pixel ratio for scaling. Future setVideoRect({ required int left, required int top, diff --git a/lib/mpv/video.dart b/lib/mpv/video.dart index 458cfae9..09659b4f 100644 --- a/lib/mpv/video.dart +++ b/lib/mpv/video.dart @@ -20,13 +20,8 @@ import 'player/video_rect_support.dart'; /// ) /// ``` class Video extends StatefulWidget { - /// The player instance. final Player player; - - /// Builder for custom video controls overlay. final Widget Function(BuildContext context)? controls; - - /// Background color shown behind the video. final Color backgroundColor; const Video({super.key, required this.player, this.controls, this.backgroundColor = Colors.black}); @@ -74,15 +69,11 @@ class _VideoState extends State