refactor: format
This commit is contained in:
@@ -180,11 +180,13 @@ class StorageService {
|
||||
}
|
||||
|
||||
Map<String, String> getLibraryFilters({String? sectionId}) {
|
||||
final scopedKey =
|
||||
sectionId != null ? 'library_filters_$sectionId' : _keyLibraryFilters;
|
||||
final scopedKey = sectionId != null
|
||||
? 'library_filters_$sectionId'
|
||||
: _keyLibraryFilters;
|
||||
|
||||
// Prefer per-library filters when available
|
||||
final jsonString = _prefs.getString(scopedKey) ??
|
||||
final jsonString =
|
||||
_prefs.getString(scopedKey) ??
|
||||
// Legacy support: fall back to global filters if present
|
||||
_prefs.getString(_keyLibraryFilters);
|
||||
if (jsonString == null) return {};
|
||||
|
||||
@@ -62,13 +62,13 @@ Future<void> playCollectionOrPlaylist({
|
||||
// Set play queue in provider
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(
|
||||
fetchedQueue,
|
||||
ratingKey,
|
||||
);
|
||||
await playbackState.setPlaybackFromPlayQueue(fetchedQueue, ratingKey);
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(context, metadata: fetchedQueue.items!.first);
|
||||
await navigateToVideoPlayer(
|
||||
context,
|
||||
metadata: fetchedQueue.items!.first,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,7 @@ Future<void> playCollectionOrPlaylist({
|
||||
playQueue.items!.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.messages.failedToCreatePlayQueueNoItems),
|
||||
),
|
||||
SnackBar(content: Text(t.messages.failedToCreatePlayQueueNoItems)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -91,10 +89,7 @@ Future<void> playCollectionOrPlaylist({
|
||||
// Set play queue in provider
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(
|
||||
playQueue,
|
||||
ratingKey,
|
||||
);
|
||||
await playbackState.setPlaybackFromPlayQueue(playQueue, ratingKey);
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(context, metadata: playQueue.items!.first);
|
||||
|
||||
@@ -22,8 +22,5 @@ String mapDioErrorToMessage(DioException error, {required String context}) {
|
||||
/// Generic fallback for unexpected errors.
|
||||
String mapUnexpectedErrorToMessage(dynamic error, {required String context}) {
|
||||
appLogger.e('Unexpected error in $context', error: error);
|
||||
return t.errors.failedToLoad(
|
||||
context: context,
|
||||
error: error.toString(),
|
||||
);
|
||||
return t.errors.failedToLoad(context: context, error: error.toString());
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ class GridSizeCalculator {
|
||||
) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > desktopBreakpoint;
|
||||
final isTablet = screenWidth > tabletBreakpoint && screenWidth <= desktopBreakpoint;
|
||||
final isTablet =
|
||||
screenWidth > tabletBreakpoint && screenWidth <= desktopBreakpoint;
|
||||
|
||||
switch (density) {
|
||||
case LibraryDensity.comfortable:
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'dart:async';
|
||||
/// Notifier for triggering refreshes of library tabs
|
||||
/// Singleton pattern for global access
|
||||
class LibraryRefreshNotifier {
|
||||
static final LibraryRefreshNotifier _instance = LibraryRefreshNotifier._internal();
|
||||
static final LibraryRefreshNotifier _instance =
|
||||
LibraryRefreshNotifier._internal();
|
||||
|
||||
factory LibraryRefreshNotifier() => _instance;
|
||||
|
||||
|
||||
@@ -57,10 +57,7 @@ class ContentStateBuilder<T> extends StatelessWidget {
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: onRetry,
|
||||
child: Text(t.common.retry),
|
||||
),
|
||||
ElevatedButton(onPressed: onRetry, child: Text(t.common.retry)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -74,10 +71,7 @@ class ContentStateBuilder<T> extends StatelessWidget {
|
||||
children: [
|
||||
Icon(emptyIcon, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
emptyMessage,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
Text(emptyMessage, style: const TextStyle(color: Colors.white70)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -42,11 +42,8 @@ class EmptyStateWidget extends StatelessWidget {
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
if (onAction != null && actionLabel != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -31,19 +31,15 @@ class ErrorStateWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Icon(icon, size: 64, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -113,7 +113,9 @@ class FolderTreeItem extends StatelessWidget {
|
||||
item.year.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -15,11 +15,7 @@ class FolderTreeView extends StatefulWidget {
|
||||
final String libraryKey;
|
||||
final void Function(String)? onRefresh;
|
||||
|
||||
const FolderTreeView({
|
||||
super.key,
|
||||
required this.libraryKey,
|
||||
this.onRefresh,
|
||||
});
|
||||
const FolderTreeView({super.key, required this.libraryKey, this.onRefresh});
|
||||
|
||||
@override
|
||||
State<FolderTreeView> createState() => _FolderTreeViewState();
|
||||
@@ -109,7 +105,9 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
_loadingFolders.remove(folder.key);
|
||||
});
|
||||
|
||||
appLogger.d('Loaded ${children.length} children for folder: ${folder.title}');
|
||||
appLogger.d(
|
||||
'Loaded ${children.length} children for folder: ${folder.title}',
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -148,10 +146,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
|
||||
// For episodes, start playback directly
|
||||
if (itemType == 'episode') {
|
||||
final result = await navigateToVideoPlayer(
|
||||
context,
|
||||
metadata: item,
|
||||
);
|
||||
final result = await navigateToVideoPlayer(context, metadata: item);
|
||||
if (result == true) {
|
||||
widget.onRefresh?.call(item.ratingKey);
|
||||
}
|
||||
@@ -181,11 +176,15 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
// 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';
|
||||
item.type.isEmpty ||
|
||||
item.type.toLowerCase() == 'folder';
|
||||
}
|
||||
|
||||
List<Widget> _buildTreeItems(List<PlexMetadata> items, int depth, [String parentPath = '']) {
|
||||
List<Widget> _buildTreeItems(
|
||||
List<PlexMetadata> items,
|
||||
int depth, [
|
||||
String parentPath = '',
|
||||
]) {
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
@@ -260,9 +259,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadRootFolders,
|
||||
child: ListView(
|
||||
children: _buildTreeItems(_rootFolders, 0),
|
||||
),
|
||||
child: ListView(children: _buildTreeItems(_rootFolders, 0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,7 @@ class HubSection extends StatelessWidget {
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon),
|
||||
@@ -76,10 +73,10 @@ class HubSection extends StatelessWidget {
|
||||
final cardWidth = screenWidth > 1600
|
||||
? 220.0
|
||||
: screenWidth > 1200
|
||||
? 200.0
|
||||
: screenWidth > 800
|
||||
? 190.0
|
||||
: 160.0;
|
||||
? 200.0
|
||||
: screenWidth > 800
|
||||
? 190.0
|
||||
: 160.0;
|
||||
|
||||
// MediaCard has 8px padding on all sides (16px total horizontally)
|
||||
// So actual poster width is cardWidth - 16
|
||||
@@ -125,9 +122,9 @@ class HubSection extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
t.messages.noItemsAvailable,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+166
-136
@@ -31,7 +31,8 @@ class MediaContextMenu extends StatefulWidget {
|
||||
final VoidCallback? onTap;
|
||||
final Widget child;
|
||||
final bool isInContinueWatching;
|
||||
final String? collectionId; // The collection ID if displaying within a collection
|
||||
final String?
|
||||
collectionId; // The collection ID if displaying within a collection
|
||||
|
||||
const MediaContextMenu({
|
||||
super.key,
|
||||
@@ -65,7 +66,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
final itemType = isPlaylist ? 'playlist' : (metadata!.type.toLowerCase());
|
||||
final isCollection = itemType == 'collection';
|
||||
|
||||
final isPartiallyWatched = !isPlaylist &&
|
||||
final isPartiallyWatched =
|
||||
!isPlaylist &&
|
||||
metadata!.viewedLeafCount != null &&
|
||||
metadata.leafCount != null &&
|
||||
metadata.viewedLeafCount! > 0 &&
|
||||
@@ -110,108 +112,104 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
} else {
|
||||
// Regular menu items for other types
|
||||
|
||||
// Mark as Watched
|
||||
if (!metadata!.isWatched || isPartiallyWatched) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'watch',
|
||||
icon: Icons.check_circle_outline,
|
||||
label: t.mediaMenu.markAsWatched,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Mark as Watched
|
||||
if (!metadata!.isWatched || isPartiallyWatched) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'watch',
|
||||
icon: Icons.check_circle_outline,
|
||||
label: t.mediaMenu.markAsWatched,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Mark as Unwatched
|
||||
if (metadata.isWatched || isPartiallyWatched) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'unwatch',
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: t.mediaMenu.markAsUnwatched,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Mark as Unwatched
|
||||
if (metadata.isWatched || isPartiallyWatched) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'unwatch',
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: t.mediaMenu.markAsUnwatched,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove from Continue Watching (only in continue watching section)
|
||||
if (widget.isInContinueWatching) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'remove_from_continue_watching',
|
||||
icon: Icons.close,
|
||||
label: t.mediaMenu.removeFromContinueWatching,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Remove from Continue Watching (only in continue watching section)
|
||||
if (widget.isInContinueWatching) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'remove_from_continue_watching',
|
||||
icon: Icons.close,
|
||||
label: t.mediaMenu.removeFromContinueWatching,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove from Collection (only when viewing items within a collection)
|
||||
if (widget.collectionId != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'remove_from_collection',
|
||||
icon: Icons.delete_outline,
|
||||
label: t.collections.removeFromCollection,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Remove from Collection (only when viewing items within a collection)
|
||||
if (widget.collectionId != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'remove_from_collection',
|
||||
icon: Icons.delete_outline,
|
||||
label: t.collections.removeFromCollection,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Go to Series (for episodes and seasons)
|
||||
if ((itemType == 'episode' || itemType == 'season') &&
|
||||
metadata.grandparentTitle != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'series',
|
||||
icon: Icons.tv,
|
||||
label: t.mediaMenu.goToSeries,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Go to Series (for episodes and seasons)
|
||||
if ((itemType == 'episode' || itemType == 'season') &&
|
||||
metadata.grandparentTitle != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'series',
|
||||
icon: Icons.tv,
|
||||
label: t.mediaMenu.goToSeries,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Go to Season (for episodes)
|
||||
if (itemType == 'episode' && metadata.parentTitle != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'season',
|
||||
icon: Icons.playlist_play,
|
||||
label: t.mediaMenu.goToSeason,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Go to Season (for episodes)
|
||||
if (itemType == 'episode' && metadata.parentTitle != null) {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'season',
|
||||
icon: Icons.playlist_play,
|
||||
label: t.mediaMenu.goToSeason,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Shuffle Play (for shows and seasons)
|
||||
if (itemType == 'show' || itemType == 'season') {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'shuffle_play',
|
||||
icon: Icons.shuffle,
|
||||
label: t.mediaMenu.shufflePlay,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Shuffle Play (for shows and seasons)
|
||||
if (itemType == 'show' || itemType == 'season') {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'shuffle_play',
|
||||
icon: Icons.shuffle,
|
||||
label: t.mediaMenu.shufflePlay,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// File Info (for episodes and movies)
|
||||
if (itemType == 'episode' || itemType == 'movie') {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'fileinfo',
|
||||
icon: Icons.info_outline,
|
||||
label: t.mediaMenu.fileInfo,
|
||||
),
|
||||
);
|
||||
}
|
||||
// File Info (for episodes and movies)
|
||||
if (itemType == 'episode' || itemType == 'movie') {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'fileinfo',
|
||||
icon: Icons.info_outline,
|
||||
label: t.mediaMenu.fileInfo,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add to... (for episodes, movies, shows, and seasons)
|
||||
if (itemType == 'episode' ||
|
||||
itemType == 'movie' ||
|
||||
itemType == 'show' ||
|
||||
itemType == 'season') {
|
||||
menuActions.add(
|
||||
_MenuAction(
|
||||
value: 'add_to',
|
||||
icon: Icons.add,
|
||||
label: t.common.addTo,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Add to... (for episodes, movies, shows, and seasons)
|
||||
if (itemType == 'episode' ||
|
||||
itemType == 'movie' ||
|
||||
itemType == 'show' ||
|
||||
itemType == 'season') {
|
||||
menuActions.add(
|
||||
_MenuAction(value: 'add_to', icon: Icons.add, label: t.common.addTo),
|
||||
);
|
||||
}
|
||||
} // End of regular menu items else block
|
||||
|
||||
String? selected;
|
||||
@@ -478,10 +476,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => FileInfoBottomSheet(
|
||||
fileInfo: fileInfo,
|
||||
title: metadata.title,
|
||||
),
|
||||
builder: (context) =>
|
||||
FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.title),
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -722,7 +718,9 @@ 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);
|
||||
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');
|
||||
@@ -732,8 +730,12 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
// Last resort: try to get it from the item's parent (for episodes/seasons)
|
||||
if (sectionId == null && metadata.grandparentRatingKey != null) {
|
||||
try {
|
||||
appLogger.d(' - Trying to get from parent: ${metadata.grandparentRatingKey}');
|
||||
final parentMeta = await client.getMetadata(metadata.grandparentRatingKey!);
|
||||
appLogger.d(
|
||||
' - Trying to get from parent: ${metadata.grandparentRatingKey}',
|
||||
);
|
||||
final parentMeta = await client.getMetadata(
|
||||
metadata.grandparentRatingKey!,
|
||||
);
|
||||
sectionId = parentMeta?.librarySectionID;
|
||||
appLogger.d(' - Parent sectionId: $sectionId');
|
||||
} catch (e) {
|
||||
@@ -747,7 +749,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Unable to determine library section for this item'),
|
||||
content: Text(
|
||||
'Unable to determine library section for this item',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -755,14 +759,17 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
// Load collections for this library section
|
||||
final collections = await client.getLibraryCollections(sectionId.toString());
|
||||
final collections = await client.getLibraryCollections(
|
||||
sectionId.toString(),
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Show dialog to select collection or create new
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => _CollectionSelectionDialog(collections: collections),
|
||||
builder: (context) =>
|
||||
_CollectionSelectionDialog(collections: collections),
|
||||
);
|
||||
|
||||
if (result == null || !context.mounted) return;
|
||||
@@ -778,7 +785,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
builder: (context) => _CreateCollectionDialog(),
|
||||
);
|
||||
|
||||
if (collectionName == null || collectionName.isEmpty || !context.mounted) {
|
||||
if (collectionName == null ||
|
||||
collectionName.isEmpty ||
|
||||
!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -800,7 +809,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
break;
|
||||
}
|
||||
|
||||
appLogger.d('Creating collection "$collectionName" with type $collectionType');
|
||||
appLogger.d(
|
||||
'Creating collection "$collectionName" with type $collectionType',
|
||||
);
|
||||
final newCollectionId = await client.createCollection(
|
||||
sectionId: sectionId.toString(),
|
||||
title: collectionName,
|
||||
@@ -810,10 +821,14 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
|
||||
if (context.mounted) {
|
||||
if (newCollectionId != null) {
|
||||
appLogger.d('Successfully created collection with ID: $newCollectionId');
|
||||
appLogger.d(
|
||||
'Successfully created collection with ID: $newCollectionId',
|
||||
);
|
||||
|
||||
// Now add the item to the newly created collection
|
||||
appLogger.d('Adding item to new collection $newCollectionId with URI: $itemUri');
|
||||
appLogger.d(
|
||||
'Adding item to new collection $newCollectionId with URI: $itemUri',
|
||||
);
|
||||
final addSuccess = await client.addToCollection(
|
||||
collectionId: newCollectionId,
|
||||
uri: itemUri,
|
||||
@@ -828,15 +843,15 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
LibraryRefreshNotifier().notifyCollectionsChanged();
|
||||
} else {
|
||||
appLogger.e('Failed to add item to new collection');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.collections.errorAddingToCollection)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.collections.errorAddingToCollection)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
appLogger.e('Failed to create collection - API returned null');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.collections.errorAddingToCollection)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.collections.errorAddingToCollection)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -850,18 +865,18 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
appLogger.d('Successfully added item(s) to collection $result');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.collections.addedToCollection)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.collections.addedToCollection)),
|
||||
);
|
||||
// Trigger refresh of collections tab
|
||||
LibraryRefreshNotifier().notifyCollectionsChanged();
|
||||
} else {
|
||||
appLogger.e(
|
||||
'Failed to add item(s) to collection $result - API returned false',
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.collections.errorAddingToCollection)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t.collections.errorAddingToCollection)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -874,7 +889,9 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${t.collections.errorAddingToCollection}: ${e.toString()}'),
|
||||
content: Text(
|
||||
'${t.collections.errorAddingToCollection}: ${e.toString()}',
|
||||
),
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
@@ -883,7 +900,10 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
/// Handle remove from collection action
|
||||
Future<void> _handleRemoveFromCollection(BuildContext context, PlexMetadata metadata) async {
|
||||
Future<void> _handleRemoveFromCollection(
|
||||
BuildContext context,
|
||||
PlexMetadata metadata,
|
||||
) async {
|
||||
final client = context.client;
|
||||
if (client == null) return;
|
||||
|
||||
@@ -928,9 +948,7 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.collections.removedFromCollection),
|
||||
),
|
||||
SnackBar(content: Text(t.collections.removedFromCollection)),
|
||||
);
|
||||
// Trigger refresh of collections tab
|
||||
LibraryRefreshNotifier().notifyCollectionsChanged();
|
||||
@@ -938,9 +956,7 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
widget.onListRefresh?.call();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.collections.removeFromCollectionFailed),
|
||||
),
|
||||
SnackBar(content: Text(t.collections.removeFromCollectionFailed)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -959,7 +975,11 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
/// Handle play action for collections and playlists
|
||||
Future<void> _handlePlay(BuildContext context, bool isCollection, bool isPlaylist) async {
|
||||
Future<void> _handlePlay(
|
||||
BuildContext context,
|
||||
bool isCollection,
|
||||
bool isPlaylist,
|
||||
) async {
|
||||
final client = context.client;
|
||||
if (client == null) return;
|
||||
|
||||
@@ -972,7 +992,11 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
/// Handle shuffle action for collections and playlists
|
||||
Future<void> _handleShuffle(BuildContext context, bool isCollection, bool isPlaylist) async {
|
||||
Future<void> _handleShuffle(
|
||||
BuildContext context,
|
||||
bool isCollection,
|
||||
bool isPlaylist,
|
||||
) async {
|
||||
final client = context.client;
|
||||
if (client == null) return;
|
||||
|
||||
@@ -985,13 +1009,18 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
}
|
||||
|
||||
/// Handle delete action for collections and playlists
|
||||
Future<void> _handleDelete(BuildContext context, bool isCollection, bool isPlaylist) async {
|
||||
Future<void> _handleDelete(
|
||||
BuildContext context,
|
||||
bool isCollection,
|
||||
bool isPlaylist,
|
||||
) async {
|
||||
final client = context.client;
|
||||
if (client == null) return;
|
||||
|
||||
final itemTitle = widget.item.title;
|
||||
final itemTypeLabel =
|
||||
isCollection ? t.collections.collection : t.playlists.playlist;
|
||||
final itemTypeLabel = isCollection
|
||||
? t.collections.collection
|
||||
: t.playlists.playlist;
|
||||
|
||||
// Show confirmation dialog
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -1241,7 +1270,8 @@ class _CollectionSelectionDialog extends StatelessWidget {
|
||||
/// Dialog to create a new collection
|
||||
class _CreateCollectionDialog extends StatefulWidget {
|
||||
@override
|
||||
State<_CreateCollectionDialog> createState() => _CreateCollectionDialogState();
|
||||
State<_CreateCollectionDialog> createState() =>
|
||||
_CreateCollectionDialogState();
|
||||
}
|
||||
|
||||
class _CreateCollectionDialogState extends State<_CreateCollectionDialog> {
|
||||
|
||||
@@ -138,10 +138,7 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
: null,
|
||||
leading: Radio<PlexSort>(value: sort, toggleable: false),
|
||||
onTap: () {
|
||||
_handleSortChange(
|
||||
sort,
|
||||
sort.isDefaultDescending,
|
||||
);
|
||||
_handleSortChange(sort, sort.isDefaultDescending);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
|
||||
/// Bottom sheet for adjusting audio sync offset
|
||||
class AudioSyncSheet extends StatefulWidget {
|
||||
@@ -14,23 +15,12 @@ class AudioSyncSheet extends StatefulWidget {
|
||||
required this.initialOffset,
|
||||
});
|
||||
|
||||
static BoxConstraints getBottomSheetConstraints(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isDesktop = size.width > 600;
|
||||
|
||||
return BoxConstraints(
|
||||
maxWidth: isDesktop ? 700 : double.infinity,
|
||||
maxHeight: isDesktop ? 400 : size.height * 0.75,
|
||||
minHeight: isDesktop ? 300 : size.height * 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
static void show(BuildContext context, Player player, int initialOffset) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.grey[900],
|
||||
isScrollControlled: true,
|
||||
constraints: getBottomSheetConstraints(context),
|
||||
constraints: BaseVideoControlSheet.getBottomSheetConstraints(context),
|
||||
builder: (context) =>
|
||||
AudioSyncSheet(player: player, initialOffset: initialOffset),
|
||||
);
|
||||
|
||||
@@ -49,7 +49,8 @@ class ChapterSheet extends StatelessWidget {
|
||||
for (int i = 0; i < chapters.length; i++) {
|
||||
final chapter = chapters[i];
|
||||
final startMs = chapter.startTimeOffset ?? 0;
|
||||
final endMs = chapter.endTimeOffset ??
|
||||
final endMs =
|
||||
chapter.endTimeOffset ??
|
||||
(i < chapters.length - 1
|
||||
? chapters[i + 1].startTimeOffset ?? 0
|
||||
: double.maxFinite.toInt());
|
||||
@@ -94,22 +95,16 @@ class ChapterSheet extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
client.getThumbnailUrl(
|
||||
chapter.thumb,
|
||||
),
|
||||
client.getThumbnailUrl(chapter.thumb),
|
||||
width: 60,
|
||||
height: 34,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (
|
||||
context,
|
||||
error,
|
||||
stackTrace,
|
||||
) =>
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Icon(
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
),
|
||||
Icons.image,
|
||||
color: Colors.white54,
|
||||
size: 34,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -118,9 +113,7 @@ class ChapterSheet extends StatelessWidget {
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
@@ -150,10 +143,7 @@ class ChapterSheet extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
trailing: isCurrentChapter
|
||||
? const Icon(
|
||||
Icons.play_circle_filled,
|
||||
color: Colors.blue,
|
||||
)
|
||||
? const Icon(Icons.play_circle_filled, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
player.seek(chapter.startTime);
|
||||
|
||||
@@ -36,8 +36,9 @@ class PlaybackSpeedSheet extends StatelessWidget {
|
||||
final isSelected = (currentRate - speed).abs() < 0.01;
|
||||
|
||||
// Format speed label
|
||||
final label =
|
||||
speed == 1.0 ? 'Normal' : '${speed.toStringAsFixed(2)}x';
|
||||
final label = speed == 1.0
|
||||
? 'Normal'
|
||||
: '${speed.toStringAsFixed(2)}x';
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
|
||||
@@ -67,15 +67,13 @@ class SubtitleTrackSheet extends StatelessWidget {
|
||||
title: Text(
|
||||
'Off',
|
||||
style: TextStyle(
|
||||
color:
|
||||
isOffSelected ? Colors.blue : Colors.white,
|
||||
color: isOffSelected
|
||||
? Colors.blue
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
trailing: isOffSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.blue,
|
||||
)
|
||||
? const Icon(Icons.check, color: Colors.blue)
|
||||
: null,
|
||||
onTap: () {
|
||||
player.setSubtitleTrack(SubtitleTrack.no());
|
||||
@@ -107,8 +105,7 @@ class SubtitleTrackSheet extends StatelessWidget {
|
||||
codecName = 'SRT';
|
||||
} else if (codecName == 'DVD_SUBTITLE') {
|
||||
codecName = 'DVD';
|
||||
} else if (codecName == 'ASS' ||
|
||||
codecName == 'SSA') {
|
||||
} else if (codecName == 'ASS' || codecName == 'SSA') {
|
||||
codecName = codecName; // Keep as-is
|
||||
} else if (codecName == 'WEBVTT') {
|
||||
codecName = 'VTT';
|
||||
@@ -116,8 +113,9 @@ class SubtitleTrackSheet extends StatelessWidget {
|
||||
parts.add(codecName);
|
||||
}
|
||||
|
||||
final label =
|
||||
parts.isEmpty ? 'Track $index' : parts.join(' · ');
|
||||
final label = parts.isEmpty
|
||||
? 'Track $index'
|
||||
: parts.join(' · ');
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
|
||||
@@ -45,9 +45,7 @@ class VersionSheet extends StatelessWidget {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
version.displayLabel,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.blue : Colors.white,
|
||||
),
|
||||
style: TextStyle(color: isSelected ? Colors.blue : Colors.white),
|
||||
),
|
||||
trailing: isSelected
|
||||
? const Icon(Icons.check, color: Colors.blue)
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../../utils/platform_detector.dart';
|
||||
import '../widgets/sync_offset_control.dart';
|
||||
import '../widgets/sleep_timer_content.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
|
||||
enum _SettingsView { menu, speed, sleep, audioSync, subtitleSync, audioDevice }
|
||||
|
||||
@@ -39,21 +40,12 @@ class _SettingsMenuItem extends StatelessWidget {
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: isHighlighted ? Colors.amber : Colors.white70,
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
leading: Icon(icon, color: isHighlighted ? Colors.amber : Colors.white70),
|
||||
title: Text(title, style: const TextStyle(color: Colors.white)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (allowValueOverflow)
|
||||
Flexible(child: valueWidget)
|
||||
else
|
||||
valueWidget,
|
||||
if (allowValueOverflow) Flexible(child: valueWidget) else valueWidget,
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.chevron_right, color: Colors.white70),
|
||||
],
|
||||
@@ -76,17 +68,6 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
required this.subtitleSyncOffset,
|
||||
});
|
||||
|
||||
static BoxConstraints getBottomSheetConstraints(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isDesktop = size.width > 600;
|
||||
|
||||
return BoxConstraints(
|
||||
maxWidth: isDesktop ? 700 : double.infinity,
|
||||
maxHeight: isDesktop ? 400 : size.height * 0.75,
|
||||
minHeight: isDesktop ? 300 : size.height * 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
Player player,
|
||||
@@ -97,7 +78,7 @@ class VideoSettingsSheet extends StatefulWidget {
|
||||
context: context,
|
||||
backgroundColor: Colors.grey[900],
|
||||
isScrollControlled: true,
|
||||
constraints: getBottomSheetConstraints(context),
|
||||
constraints: BaseVideoControlSheet.getBottomSheetConstraints(context),
|
||||
builder: (context) => VideoSettingsSheet(
|
||||
player: player,
|
||||
audioSyncOffset: audioSyncOffset,
|
||||
@@ -194,7 +175,6 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Widget _buildHeader() {
|
||||
final sleepTimer = SleepTimerService();
|
||||
final isIconActive =
|
||||
|
||||
@@ -1624,5 +1624,4 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user