fix: make PlexMetadata type/key/title nullable for folder items
Plex API returns Directory entries without type field, causing TypeError crash in fromJson. Made fields nullable and migrated callers to use mediaType enum and displayTitle getter instead of raw string access.
This commit is contained in:
@@ -145,7 +145,7 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
|
||||
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<DownloadTreeView> {
|
||||
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(
|
||||
|
||||
@@ -357,7 +357,7 @@ class _EpisodeCardState extends State<EpisodeCard> {
|
||||
// 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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -327,7 +327,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
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<MediaContextMenu> {
|
||||
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<MediaContextMenu> {
|
||||
|
||||
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<MediaContextMenu> {
|
||||
|
||||
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<MediaContextMenu> {
|
||||
}
|
||||
|
||||
// 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<MediaContextMenu> {
|
||||
// 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<MediaContextMenu> {
|
||||
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<MediaContextMenu> {
|
||||
Future<void> _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<MediaContextMenu> {
|
||||
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),
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -474,7 +474,7 @@ class ContentStripState extends State<ContentStrip> {
|
||||
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<ContentStrip> {
|
||||
}
|
||||
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({
|
||||
|
||||
@@ -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<String> 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,
|
||||
|
||||
Reference in New Issue
Block a user