diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index a4a6cb4d0..6074547f3 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -62,11 +62,11 @@ enum PlexMediaType { class PlexMetadata with MultiServerFields { @JsonKey(readValue: _readRatingKey) final String ratingKey; - final String key; + final String? key; final String? guid; final String? studio; - final String type; - final String title; + final String? type; + final String? title; final String? titleSort; final String? contentRating; final String? summary; @@ -135,7 +135,8 @@ class PlexMetadata with MultiServerFields { /// Parsed media type enum for type-safe comparisons PlexMediaType get mediaType { - return switch (type.toLowerCase()) { + if (type == null) return PlexMediaType.unknown; + return switch (type!.toLowerCase()) { 'movie' => PlexMediaType.movie, 'show' => PlexMediaType.show, 'season' => PlexMediaType.season, @@ -153,11 +154,11 @@ class PlexMetadata with MultiServerFields { PlexMetadata({ required this.ratingKey, - required this.key, + this.key, this.guid, this.studio, - required this.type, - required this.title, + this.type, + this.title, this.titleSort, this.contentRating, this.summary, @@ -366,7 +367,7 @@ class PlexMetadata with MultiServerFields { // Helper to get the display title (show name for episodes/seasons, title otherwise) String get displayTitle { - final itemType = type.toLowerCase(); + final itemType = type?.toLowerCase(); // For episodes and seasons, prefer grandparent title (show name) if ((itemType == 'episode' || itemType == 'season') && grandparentTitle != null) { @@ -376,12 +377,12 @@ class PlexMetadata with MultiServerFields { if (itemType == 'season' && parentTitle != null) { return parentTitle!; } - return title; + return title ?? ''; } // Helper to get the subtitle (episode/season title) String? get displaySubtitle { - final itemType = type.toLowerCase(); + final itemType = type?.toLowerCase(); if (itemType == 'episode' || itemType == 'season') { // If we showed grandparent/parent as title, show this item's title as subtitle @@ -401,7 +402,7 @@ class PlexMetadata with MultiServerFields { /// For movies/shows/seasons in mixed hub context: returns art (16:9 background) /// For other types: returns thumb String? posterThumb({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) { - final itemType = type.toLowerCase(); + final itemType = type?.toLowerCase(); if (itemType == 'episode') { switch (mode) { @@ -436,7 +437,7 @@ class PlexMetadata with MultiServerFields { /// Clips (trailers, extras) always use 16:9. /// Movies, shows, and seasons use 16:9 in mixed hub context with episodeThumbnail mode. bool usesWideAspectRatio(EpisodePosterMode mode, {bool mixedHubContext = false}) { - final itemType = type.toLowerCase(); + final itemType = type?.toLowerCase(); // Clips (trailers, extras) are always 16:9 if (itemType == 'clip') return true; if (itemType == 'episode' && mode == EpisodePosterMode.episodeThumbnail) { diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index ecf8e4fd6..8bfe8dcf1 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -8,11 +8,11 @@ part of 'plex_metadata.dart'; PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( ratingKey: _readRatingKey(json, 'ratingKey') as String, - key: json['key'] as String, + key: json['key'] as String?, guid: json['guid'] as String?, studio: json['studio'] as String?, - type: json['type'] as String, - title: json['title'] as String, + type: json['type'] as String?, + title: json['title'] as String?, titleSort: json['titleSort'] as String?, contentRating: json['contentRating'] as String?, summary: json['summary'] as String?, diff --git a/lib/providers/download_provider.dart b/lib/providers/download_provider.dart index a2bb243ae..6c79437c1 100644 --- a/lib/providers/download_provider.dart +++ b/lib/providers/download_provider.dart @@ -515,10 +515,10 @@ class DownloadProvider extends ChangeNotifier { } // We have metadata, check type - final type = meta.type.toLowerCase(); - if (type == 'show') { + final mt = meta.mediaType; + if (mt == PlexMediaType.show) { return getAggregateProgressForShow(serverId, ratingKey); - } else if (type == 'season') { + } else if (mt == PlexMediaType.season) { return getAggregateProgressForSeason(serverId, ratingKey); } @@ -606,19 +606,19 @@ class DownloadProvider extends ChangeNotifier { _queueing.add(globalKey); notifyListeners(); - final type = metadata.type.toLowerCase(); + final mt = metadata.mediaType; - if (type == 'movie' || type == 'episode') { + if (mt == PlexMediaType.movie || mt == PlexMediaType.episode) { // Direct download of a single item await _queueSingleDownload(metadata, client); return 1; - } else if (type == 'show') { + } else if (mt == PlexMediaType.show) { // Store show metadata so getProgress() can identify it as a show _metadata[globalKey] = metadata; // Download all episodes from all seasons return await _queueShowDownload(metadata, client); - } else if (type == 'season') { + } else if (mt == PlexMediaType.season) { // Store season metadata so getProgress() can identify it as a season _metadata[globalKey] = metadata; @@ -765,11 +765,11 @@ class DownloadProvider extends ChangeNotifier { /// Used for resuming partial downloads /// Returns the number of episodes queued Future queueMissingEpisodes(PlexMetadata metadata, PlexClient client) async { - final type = metadata.type.toLowerCase(); + final mt = metadata.mediaType; - if (type == 'show') { + if (mt == PlexMediaType.show) { return await _queueMissingShowEpisodes(metadata, client); - } else if (type == 'season') { + } else if (mt == PlexMediaType.season) { return await _queueMissingSeasonEpisodes(metadata, client); } else { throw Exception('queueMissingEpisodes only supports shows/seasons'); diff --git a/lib/screens/collection_detail_screen.dart b/lib/screens/collection_detail_screen.dart index 4f9e4efef..0fd68677d 100644 --- a/lib/screens/collection_detail_screen.dart +++ b/lib/screens/collection_detail_screen.dart @@ -31,7 +31,7 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen widget.collection; @override - String get title => widget.collection.title; + String get title => widget.collection.title!; @override String get emptyMessage => t.collections.empty; @@ -93,7 +93,7 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen Widget _buildHeroItem(PlexMetadata heroItem, double heroHeight) { final isEpisode = heroItem.isEpisode; - final showName = heroItem.grandparentTitle ?? heroItem.title; + final showName = heroItem.grandparentTitle ?? heroItem.displayTitle; final screenWidth = MediaQuery.of(context).size.width; final isLargeScreen = ScreenBreakpoints.isWideTabletOrLarger(screenWidth); diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index cd259f1c4..f893168d2 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -180,7 +180,7 @@ class _HubDetailScreenState extends State switch (sortKey) { case 'titleSort': case 'title': - comparison = a.title.compareTo(b.title); + comparison = (a.title ?? '').compareTo(b.title ?? ''); break; case 'addedAt': comparison = (a.addedAt ?? 0).compareTo(b.addedAt ?? 0); @@ -193,7 +193,7 @@ class _HubDetailScreenState extends State comparison = (a.rating ?? 0).compareTo(b.rating ?? 0); break; default: - comparison = a.title.compareTo(b.title); + comparison = (a.title ?? '').compareTo(b.title ?? ''); } return _isSortDescending ? -comparison : comparison; diff --git a/lib/screens/libraries/folder_tree_item.dart b/lib/screens/libraries/folder_tree_item.dart index 640c96a54..23c8d9342 100644 --- a/lib/screens/libraries/folder_tree_item.dart +++ b/lib/screens/libraries/folder_tree_item.dart @@ -42,21 +42,14 @@ class FolderTreeItem extends StatelessWidget { } // File icons based on type - final type = item.type.toLowerCase(); - switch (type) { - case 'movie': - return Symbols.movie_rounded; - case 'show': - return Symbols.tv_rounded; - case 'season': - return Symbols.video_library_rounded; - case 'episode': - return Symbols.play_circle_rounded; - case 'collection': - return Symbols.collections_rounded; - default: - return Symbols.insert_drive_file_rounded; - } + return switch (item.mediaType) { + PlexMediaType.movie => Symbols.movie_rounded, + PlexMediaType.show => Symbols.tv_rounded, + PlexMediaType.season => Symbols.video_library_rounded, + PlexMediaType.episode => Symbols.play_circle_rounded, + PlexMediaType.collection => Symbols.collections_rounded, + _ => Symbols.insert_drive_file_rounded, + }; } void _handleTap() { @@ -104,7 +97,7 @@ class FolderTreeItem extends StatelessWidget { // Item title Expanded( child: Text( - item.title, + item.displayTitle, style: TextStyle(fontSize: 14, fontWeight: isFolder ? FontWeight.w500 : FontWeight.w400), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/screens/libraries/folder_tree_view.dart b/lib/screens/libraries/folder_tree_view.dart index 41ad612ef..bb2a612e3 100644 --- a/lib/screens/libraries/folder_tree_view.dart +++ b/lib/screens/libraries/folder_tree_view.dart @@ -87,32 +87,32 @@ class _FolderTreeViewState extends State { Future _loadFolderChildren(PlexMetadata folder) async { // Already loading this folder - if (_loadingFolders.contains(folder.key)) return; + if (_loadingFolders.contains(folder.key!)) return; // Already loaded and cached - if (_childrenCache.containsKey(folder.key)) { + if (_childrenCache.containsKey(folder.key!)) { setState(() { - _expandedFolders.add(folder.key); + _expandedFolders.add(folder.key!); }); return; } setState(() { - _loadingFolders.add(folder.key); + _loadingFolders.add(folder.key!); }); try { final client = context.getClientForServer(widget.serverId!); // Items are automatically tagged with server info by PlexClient - final children = await client.getFolderChildren(folder.key); + final children = await client.getFolderChildren(folder.key!); if (!mounted) return; setState(() { - _childrenCache[folder.key] = children; - _expandedFolders.add(folder.key); - _loadingFolders.remove(folder.key); + _childrenCache[folder.key!] = children; + _expandedFolders.add(folder.key!); + _loadingFolders.remove(folder.key!); }); appLogger.d('Loaded ${children.length} children for folder: ${folder.title}'); @@ -121,7 +121,7 @@ class _FolderTreeViewState extends State { appLogger.e('Failed to load folder children', error: e); setState(() { - _loadingFolders.remove(folder.key); + _loadingFolders.remove(folder.key!); }); if (mounted) { @@ -131,9 +131,9 @@ class _FolderTreeViewState extends State { } void _toggleFolder(PlexMetadata folder) { - if (_expandedFolders.contains(folder.key)) { + if (_expandedFolders.contains(folder.key!)) { setState(() { - _expandedFolders.remove(folder.key); + _expandedFolders.remove(folder.key!); }); } else { _loadFolderChildren(folder); @@ -147,19 +147,19 @@ class _FolderTreeViewState extends State { Future _handleFolderPlay(PlexMetadata folder) async { final client = context.getClientForServer(widget.serverId!); final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId); - await launcher.launchFromFolder(folderKey: folder.key, shuffle: false); + await launcher.launchFromFolder(folderKey: folder.key!, shuffle: false); } Future _handleFolderShuffle(PlexMetadata folder) async { final client = context.getClientForServer(widget.serverId!); final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId); - await launcher.launchFromFolder(folderKey: folder.key, shuffle: true); + await launcher.launchFromFolder(folderKey: folder.key!, shuffle: true); } bool _isFolder(PlexMetadata item) { // Folders typically don't have a specific type or might have special indicators // Check for common folder indicators - return item.key.contains('/folder') || item.type.isEmpty || item.type.toLowerCase() == 'folder'; + return item.key?.contains('/folder') == true || item.type == null || item.type!.isEmpty || item.mediaType == PlexMediaType.unknown; } List _buildTreeItems(List items, int depth, [String parentPath = '']) { diff --git a/lib/screens/livetv/tabs/whats_on_tab.dart b/lib/screens/livetv/tabs/whats_on_tab.dart index e888fee24..1641d17d8 100644 --- a/lib/screens/livetv/tabs/whats_on_tab.dart +++ b/lib/screens/livetv/tabs/whats_on_tab.dart @@ -11,6 +11,7 @@ import '../../../focus/locked_hub_controller.dart'; import '../../../i18n/strings.g.dart'; import '../../../models/livetv_channel.dart'; import '../../../models/livetv_hub_result.dart'; +import '../../../models/plex_metadata.dart'; import '../../../providers/multi_server_provider.dart'; import '../../../providers/settings_provider.dart'; import '../../../services/settings_service.dart' show LibraryDensity; @@ -168,12 +169,12 @@ class WhatsOnTabState extends State { if (entry.program.isCurrentlyAiring && channel != null) { // Live → play directly _tuneChannel(channel); - } else if (entry.metadata.type.toLowerCase() == 'show') { + } else if (entry.metadata.mediaType == PlexMediaType.show) { // Show with upcoming episodes → show full schedule Navigator.of(context).push( MaterialPageRoute( builder: (_) => LiveTvShowScheduleScreen( - showTitle: entry.metadata.title, + showTitle: entry.metadata.displayTitle, serverId: entry.metadata.serverId ?? '', channels: widget.channels, ), diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index a53d3d492..c94db2f1b 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -740,7 +740,7 @@ class _MediaDetailScreenState extends State final confirmed = await showDeleteConfirmation( context, title: t.downloads.deleteDownload, - message: t.downloads.deleteConfirm(title: metadata.title), + message: t.downloads.deleteConfirm(title: metadata.displayTitle), ); if (confirmed && context.mounted) { @@ -1579,7 +1579,7 @@ class _MediaDetailScreenState extends State onSecondaryTapDown: (details) => tapPosition = details.globalPosition, onSecondaryTap: () => _showSeasonTabContextMenu(index, position: tapPosition), child: FocusableTabChip( - label: season.title, + label: season.title!, isSelected: index == _selectedSeasonIndex, focusNode: _seasonTabFocusNodes.length > index ? _seasonTabFocusNodes[index] : null, onSelect: () { @@ -2251,11 +2251,11 @@ class _MediaDetailScreenState extends State fit: BoxFit.contain, alignment: Alignment.centerLeft, errorBuilder: (context, error, stackTrace) => - _buildTitleText(context, metadata.title), + _buildTitleText(context, metadata.displayTitle), ); } // Offline but no local file - show title text - return _buildTitleText(context, metadata.title); + return _buildTitleText(context, metadata.displayTitle); } // Online - use network image @@ -2280,7 +2280,7 @@ class _MediaDetailScreenState extends State placeholder: (context, url) => Align( alignment: Alignment.centerLeft, child: Text( - metadata.title, + metadata.displayTitle, style: Theme.of(context).textTheme.displaySmall?.copyWith( color: Colors.white.withValues(alpha: 0.3), fontWeight: FontWeight.bold, @@ -2293,7 +2293,7 @@ class _MediaDetailScreenState extends State ), ), errorWidget: (context, url, error) { - return _buildTitleText(context, metadata.title); + return _buildTitleText(context, metadata.displayTitle); }, ), sigma: 10, @@ -2304,7 +2304,7 @@ class _MediaDetailScreenState extends State ) else Text( - metadata.title, + metadata.displayTitle, style: Theme.of(context).textTheme.displaySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, diff --git a/lib/screens/playlist/playlist_item_card.dart b/lib/screens/playlist/playlist_item_card.dart index c10d61c06..7b6763561 100644 --- a/lib/screens/playlist/playlist_item_card.dart +++ b/lib/screens/playlist/playlist_item_card.dart @@ -237,9 +237,9 @@ class _PlaylistItemCardState extends State { } String _buildSubtitle() { - final itemType = widget.item.type.toLowerCase(); + final itemType = widget.item.mediaType; - if (itemType == 'episode') { + if (itemType == PlexMediaType.episode) { // For episodes, show "S#E# - Episode Title" final season = widget.item.parentIndex; final episode = widget.item.index; @@ -247,7 +247,7 @@ class _PlaylistItemCardState extends State { return 'S${season}E$episode${widget.item.displaySubtitle != null ? ' - ${widget.item.displaySubtitle}' : ''}'; } return widget.item.displaySubtitle ?? t.discover.tvShow; - } else if (itemType == 'movie') { + } else if (itemType == PlexMediaType.movie) { // For movies, show year and edition final year = widget.item.year?.toString(); if (year != null && widget.item.editionTitle != null) { @@ -257,6 +257,6 @@ class _PlaylistItemCardState extends State { } // Default to type - return widget.item.type; + return widget.item.mediaType.name; } } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index e67610aff..8ba3e2de9 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1485,7 +1485,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin watchTogether.setCurrentMedia( ratingKey: targetMetadata.ratingKey, serverId: targetMetadata.serverId!, - mediaTitle: targetMetadata.title, + mediaTitle: targetMetadata.displayTitle, ); } } catch (e) { @@ -2898,7 +2898,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin ) else Text( - _nextEpisode!.title, + _nextEpisode!.title!, style: const TextStyle( color: Colors.white, fontSize: 14, diff --git a/lib/services/discord_rpc_service.dart b/lib/services/discord_rpc_service.dart index e3fe6c619..b6e255977 100644 --- a/lib/services/discord_rpc_service.dart +++ b/lib/services/discord_rpc_service.dart @@ -351,7 +351,7 @@ class DiscordRPCService { timestamps: _buildTimestamps(), statusDisplayType: DiscordStatusDisplayType.details, largeAsset: _cachedThumbnailUrl != null - ? DiscordAsset(url: _cachedThumbnailUrl!, text: metadata.grandparentTitle ?? metadata.title) + ? DiscordAsset(url: _cachedThumbnailUrl!, text: metadata.grandparentTitle ?? metadata.title!) : null, ), ); @@ -392,14 +392,14 @@ class DiscordRPCService { switch (metadata.mediaType) { case PlexMediaType.movie: final year = metadata.year != null ? ' (${metadata.year})' : ''; - return metadata.title + year; + return metadata.title! + year; case PlexMediaType.episode: // Show: "Show Name" or just episode title if no show name - return metadata.grandparentTitle ?? metadata.title; + return metadata.grandparentTitle ?? metadata.title!; default: - return metadata.title; + return metadata.title!; } } @@ -411,9 +411,9 @@ class DiscordRPCService { final season = metadata.parentIndex; final episode = metadata.index; if (season != null && episode != null) { - return 'S$season E$episode - ${metadata.title}'; + return 'S$season E$episode - ${metadata.title!}'; } - return metadata.title; + return metadata.title!; case PlexMediaType.movie: return metadata.studio; diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 6245b8736..079a608cb 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -341,7 +341,7 @@ class DownloadManagerService { serverId: metadata.serverId!, ratingKey: metadata.ratingKey, globalKey: globalKey, - type: metadata.type, + type: metadata.type ?? '', parentRatingKey: metadata.parentRatingKey, grandparentRatingKey: metadata.grandparentRatingKey, status: DownloadStatus.queued.index, @@ -446,8 +446,8 @@ class DownloadManagerService { // Build display name for notifications final displayName = metadata.type == 'episode' - ? '${metadata.grandparentTitle ?? metadata.title} - ${metadata.title}' - : metadata.title; + ? '${metadata.grandparentTitle ?? metadata.displayTitle} - ${metadata.displayTitle}' + : metadata.displayTitle; // Get WiFi-only setting for native enforcement final settings = await SettingsService.getInstance(); @@ -1234,7 +1234,7 @@ class DownloadManagerService { // Emit initial progress _emitDeletionProgress( - DeletionProgress(globalKey: globalKey, itemTitle: metadata.title, currentItem: 0, totalItems: totalItems), + DeletionProgress(globalKey: globalKey, itemTitle: metadata.displayTitle, currentItem: 0, totalItems: totalItems), ); // Delete files from storage (with progress updates) @@ -1250,7 +1250,7 @@ class DownloadManagerService { _emitDeletionProgress( DeletionProgress( globalKey: globalKey, - itemTitle: metadata.title, + itemTitle: metadata.displayTitle, currentItem: totalItems, totalItems: totalItems, ), @@ -1264,17 +1264,14 @@ class DownloadManagerService { /// Calculate total items to delete (for progress tracking) Future _getTotalItemsToDelete(PlexMetadata metadata, String _) async { - switch (metadata.type.toLowerCase()) { - case 'episode': - return 1; // Single episode - case 'movie': - return 1; // Single movie - case 'season': - // Count episodes in season + switch (metadata.mediaType) { + case PlexMediaType.episode: + case PlexMediaType.movie: + return 1; + case PlexMediaType.season: final episodes = await _database.getEpisodesBySeason(metadata.ratingKey); return episodes.length; - case 'show': - // Count all episodes in show + case PlexMediaType.show: final episodes = await _database.getEpisodesByShow(metadata.ratingKey); return episodes.length; default: @@ -1301,17 +1298,17 @@ class DownloadManagerService { } // Delete based on type - switch (metadata.type.toLowerCase()) { - case 'episode': + switch (metadata.mediaType) { + case PlexMediaType.episode: await _deleteEpisodeFiles(metadata, serverId); break; - case 'season': + case PlexMediaType.season: await _deleteSeasonFiles(metadata, serverId); break; - case 'show': + case PlexMediaType.show: await _deleteShowFiles(metadata, serverId); break; - case 'movie': + case PlexMediaType.movie: await _deleteMovieFiles(metadata, serverId); break; default: @@ -1447,7 +1444,7 @@ class DownloadManagerService { episodes: episodesInSeason, serverId: serverId, parentKey: season.ratingKey, - parentTitle: season.title, + parentTitle: season.displayTitle, ); final seasonDir = await _storageService.getSeasonDirectory(season, showYear: showYear); @@ -1510,7 +1507,7 @@ class DownloadManagerService { episodes: episodesInShow, serverId: serverId, parentKey: show.ratingKey, - parentTitle: show.title, + parentTitle: show.displayTitle, ); final showDir = await _storageService.getShowDirectory(show); diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart index b3e10bf38..7676929d0 100644 --- a/lib/services/download_storage_service.dart +++ b/lib/services/download_storage_service.dart @@ -65,7 +65,7 @@ class DownloadStorageService { String _formatEpisodeFileName(PlexMetadata episode) { final season = padNumber(episode.parentIndex ?? 0, 2); final ep = padNumber(episode.index ?? 0, 2); - final episodeName = _sanitizeFileName(episode.title); + final episodeName = _sanitizeFileName(episode.title!); return 'S${season}E$ep - $episodeName'; } @@ -246,13 +246,13 @@ class DownloadStorageService { /// Get the folder name for a movie: "Movie Name (YYYY)" String _getMovieFolderName(PlexMetadata movie) { - return _formatTitleWithYear(movie.title, movie.year); + return _formatTitleWithYear(movie.title!, movie.year); } /// Get the folder name for a TV show: "Show Name (YYYY)" /// [showYear]: Pass explicitly for episodes (episode.year may differ from show's year) String _getShowFolderName(PlexMetadata metadata, {int? showYear}) { - final title = metadata.grandparentTitle ?? metadata.title; + final title = metadata.grandparentTitle ?? metadata.title!; final year = showYear ?? metadata.year; return _formatTitleWithYear(title, year); } diff --git a/lib/services/media_controls_manager.dart b/lib/services/media_controls_manager.dart index c07a6fb82..7248d1569 100644 --- a/lib/services/media_controls_manager.dart +++ b/lib/services/media_controls_manager.dart @@ -53,7 +53,7 @@ class MediaControlsManager { // Update OS media controls await OsMediaControls.setMetadata( MediaMetadata( - title: metadata.title, + title: metadata.title!, artist: _buildArtist(metadata), artworkUrl: artworkUrl, duration: duration, diff --git a/lib/services/watch_next_service.dart b/lib/services/watch_next_service.dart index 4c17310a4..e9957bd4d 100644 --- a/lib/services/watch_next_service.dart +++ b/lib/services/watch_next_service.dart @@ -134,7 +134,7 @@ class WatchNextService { title = item.grandparentTitle!; episodeTitle = item.title; } else { - title = item.title; + title = item.title!; episodeTitle = null; } @@ -148,7 +148,7 @@ class WatchNextService { 'episodeTitle': episodeTitle, 'description': item.summary, 'posterUri': posterUri, - 'type': item.type.toLowerCase(), + 'type': item.mediaType.name, 'duration': item.duration ?? 0, 'lastPlaybackPosition': item.viewOffset ?? 0, 'lastEngagementTime': lastEngagementTime, diff --git a/lib/utils/content_utils.dart b/lib/utils/content_utils.dart index b5d8021b1..153d24595 100644 --- a/lib/utils/content_utils.dart +++ b/lib/utils/content_utils.dart @@ -86,7 +86,7 @@ String formatContentRating(String? contentRating) { /// Extension on PlexMetadata for type checking convenience methods extension PlexMetadataType on PlexMetadata { - String get _lowerType => type.toLowerCase(); + String get _lowerType => type?.toLowerCase() ?? ''; bool get isShow => _lowerType == ContentTypes.show; bool get isMovie => _lowerType == ContentTypes.movie; diff --git a/lib/utils/deletion_notifier.dart b/lib/utils/deletion_notifier.dart index cd54bb9c6..24dea4de9 100644 --- a/lib/utils/deletion_notifier.dart +++ b/lib/utils/deletion_notifier.dart @@ -80,7 +80,7 @@ class DeletionNotifier extends BaseNotifier { ratingKey: metadata.ratingKey, serverId: metadata.serverId ?? '', parentChain: _buildParentChain(metadata), - mediaType: metadata.type, + mediaType: metadata.type ?? '', leafCount: metadata.leafCount ?? 1, isDownloadOnly: isDownloadOnly, ), diff --git a/lib/utils/watch_state_notifier.dart b/lib/utils/watch_state_notifier.dart index 207179ac5..249affc1f 100644 --- a/lib/utils/watch_state_notifier.dart +++ b/lib/utils/watch_state_notifier.dart @@ -86,7 +86,7 @@ class WatchStateNotifier extends BaseNotifier { serverId: metadata.serverId ?? '', changeType: isNowWatched ? WatchStateChangeType.watched : WatchStateChangeType.unwatched, parentChain: _buildParentChain(metadata), - mediaType: metadata.type, + mediaType: metadata.type ?? '', isNowWatched: isNowWatched, ), ); @@ -103,7 +103,7 @@ class WatchStateNotifier extends BaseNotifier { serverId: metadata.serverId ?? '', changeType: WatchStateChangeType.progressUpdate, parentChain: _buildParentChain(metadata), - mediaType: metadata.type, + mediaType: metadata.type ?? '', viewOffset: viewOffset, isNowWatched: isNowWatched, ), diff --git a/lib/widgets/download_tree_view.dart b/lib/widgets/download_tree_view.dart index c36c3683e..3e0af6e02 100644 --- a/lib/widgets/download_tree_view.dart +++ b/lib/widgets/download_tree_view.dart @@ -145,7 +145,7 @@ class _DownloadTreeViewState extends State { movies.add( DownloadTreeNode( key: globalKey, - title: meta.title, + title: meta.displayTitle, type: DownloadNodeType.movie, progress: download.progressPercent, status: download.status, @@ -202,7 +202,7 @@ class _DownloadTreeViewState extends State { if (meta == null) continue; final episodeNumber = meta.index; - final episodeTitle = episodeNumber != null ? 'Episode $episodeNumber - ${meta.title}' : meta.title; + final episodeTitle = episodeNumber != null ? 'Episode $episodeNumber - ${meta.title!}' : meta.title!; episodeNodes.add( DownloadTreeNode( diff --git a/lib/widgets/episode_card.dart b/lib/widgets/episode_card.dart index e146dea63..2faecba56 100644 --- a/lib/widgets/episode_card.dart +++ b/lib/widgets/episode_card.dart @@ -357,7 +357,7 @@ class _EpisodeCardState extends State { // Episode title Expanded( child: Text( - widget.episode.title, + widget.episode.title!, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 24cb89e2d..3384f36ed 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -518,7 +518,7 @@ class _MediaCardList extends StatelessWidget { color: tokens(context).textMuted.withValues(alpha: 0.85), fontSize: _subtitleFontSize, ); - final episodeTitle = metadata.displaySubtitle ?? metadata.title; + final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle; final episodeNum = metadata.index != null ? ' E${metadata.index}' : ''; return Row( children: [ @@ -783,7 +783,7 @@ class _MediaCardHelpers { // For episodes, show "S# · Episode Title" with clickable season link if (metadata.isEpisode && metadata.parentIndex != null) { - final episodeTitle = metadata.displaySubtitle ?? metadata.title; + final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle; if (metadata.parentRatingKey != null) { return Row( children: [ diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index ca51beb5b..eee4f8e53 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -327,7 +327,7 @@ class MediaContextMenuState extends State { context, showDragHandle: true, builder: (context) => _FocusableContextMenuSheet( - title: widget.item.title, + title: widget.item.displayTitle, actions: menuActions, focusFirstItem: openedFromKeyboard, ), @@ -597,7 +597,7 @@ class MediaContextMenuState extends State { await OverlaySheetController.showAdaptive( context, isScrollControlled: true, - builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.title), + builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.displayTitle), ); } else if (context.mounted) { showErrorSnackBar(context, t.messages.fileInfoNotAvailable); @@ -667,7 +667,7 @@ class MediaContextMenuState extends State { try { final metadata = widget.item as PlexMetadata; - final itemType = metadata.type.toLowerCase(); + final itemType = metadata.mediaType.name; // Load playlists final playlists = await client.getPlaylists(playlistType: 'video'); @@ -752,7 +752,7 @@ class MediaContextMenuState extends State { try { final metadata = widget.item as PlexMetadata; - final itemType = metadata.type.toLowerCase(); + final itemType = metadata.mediaType; // Get the library section ID from the item // First try from the metadata itself @@ -776,8 +776,8 @@ class MediaContextMenuState extends State { } // If still not found, try to extract from the key field - if (sectionId == null) { - final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key); + if (sectionId == null && metadata.key != null) { + final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key!); if (keyMatch != null) { sectionId = int.tryParse(keyMatch.group(1)!); appLogger.d(' - Extracted from key: $sectionId'); @@ -841,18 +841,20 @@ class MediaContextMenuState extends State { // Determine the collection type based on the item type int? collectionType; switch (itemType) { - case 'movie': + case PlexMediaType.movie: collectionType = 1; break; - case 'show': + case PlexMediaType.show: collectionType = 2; break; - case 'season': + case PlexMediaType.season: collectionType = 3; break; - case 'episode': + case PlexMediaType.episode: collectionType = 4; break; + default: + break; } appLogger.d('Creating collection "$collectionName" with type $collectionType'); @@ -951,7 +953,7 @@ class MediaContextMenuState extends State { final confirmed = await showDeleteConfirmation( context, title: t.collections.removeFromCollection, - message: t.collections.removeFromCollectionConfirm(title: metadata.title), + message: t.collections.removeFromCollectionConfirm(title: metadata.displayTitle), ); if (!confirmed || !context.mounted) return; @@ -1008,7 +1010,7 @@ class MediaContextMenuState extends State { Future _handleDelete(BuildContext context, bool isCollection, bool isPlaylist) async { final client = _getClientForItem(); - final itemTitle = widget.item.title; + final itemTitle = widget.item.displayTitle; final itemTypeLabel = isCollection ? t.collections.collection : t.playlists.playlist; // Show confirmation dialog @@ -1110,7 +1112,7 @@ class MediaContextMenuState extends State { final confirmed = await showDeleteConfirmation( context, title: t.downloads.deleteDownload, - message: t.downloads.deleteConfirm(title: metadata.title), + message: t.downloads.deleteConfirm(title: metadata.displayTitle), ); if (!confirmed || !context.mounted) return; @@ -1264,7 +1266,7 @@ class _CollectionSelectionDialog extends StatelessWidget { final collection = collections[index - 1]; return ListTile( leading: const AppIcon(Symbols.collections_rounded, fill: 1), - title: Text(collection.title), + title: Text(collection.title!), subtitle: collection.childCount != null ? Text('${collection.childCount} items') : null, onTap: () => Navigator.pop(context, collection.ratingKey), ); diff --git a/lib/widgets/video_controls/sheets/queue_sheet.dart b/lib/widgets/video_controls/sheets/queue_sheet.dart index 972726b22..e24c6c8e2 100644 --- a/lib/widgets/video_controls/sheets/queue_sheet.dart +++ b/lib/widgets/video_controls/sheets/queue_sheet.dart @@ -57,7 +57,7 @@ class QueueSheet extends StatelessWidget { return FocusableListTile( leading: _buildThumbnail(context, item, isCurrent, isTablet: isTablet), title: Text( - item.title, + item.title!, style: TextStyle( color: isCurrent ? primaryColor : null, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal, @@ -139,7 +139,7 @@ class QueueSheet extends StatelessWidget { if (item.year != null) { return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}'; } - return item.type; + return item.mediaType.name; } static dynamic _tryGetClient(BuildContext context, PlexMetadata item) { diff --git a/lib/widgets/video_controls/widgets/content_strip.dart b/lib/widgets/video_controls/widgets/content_strip.dart index 830eb2962..554969b2f 100644 --- a/lib/widgets/video_controls/widgets/content_strip.dart +++ b/lib/widgets/video_controls/widgets/content_strip.dart @@ -474,7 +474,7 @@ class ContentStripState extends State { const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34), ) : null, - title: item.title, + title: item.title!, subtitle: _buildQueueSubtitle(item), onTap: onTap, ); @@ -511,7 +511,7 @@ class ContentStripState extends State { } if (item.grandparentTitle != null) return item.grandparentTitle!; if (item.year != null) return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}'; - return item.type; + return item.mediaType.name; } Widget _buildStripItem({ diff --git a/lib/widgets/video_controls/widgets/video_controls_header.dart b/lib/widgets/video_controls/widgets/video_controls_header.dart index 088bc237b..95ddb8821 100644 --- a/lib/widgets/video_controls/widgets/video_controls_header.dart +++ b/lib/widgets/video_controls/widgets/video_controls_header.dart @@ -54,14 +54,14 @@ class VideoControlsHeader extends StatelessWidget { Widget _buildSingleLineTitle() { // Build single-line title combining series and episode info - final seriesName = metadata.grandparentTitle ?? metadata.title; + final seriesName = metadata.grandparentTitle ?? metadata.title!; final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null; List parts = [seriesName]; if (hasEpisodeInfo) { parts.add('S${metadata.parentIndex}E${metadata.index}'); - parts.add(metadata.title); + parts.add(metadata.title!); } return Text( @@ -78,7 +78,7 @@ class VideoControlsHeader extends StatelessWidget { if (metadata.parentIndex != null && metadata.index != null) { secondLineParts.add('S${metadata.parentIndex}'); secondLineParts.add('E${metadata.index}'); - secondLineParts.add(metadata.title); + secondLineParts.add(metadata.title!); } if (metadata.duration != null) { @@ -89,7 +89,7 @@ class VideoControlsHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - metadata.grandparentTitle ?? metadata.title, + metadata.grandparentTitle ?? metadata.title!, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis,