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