refactor: fix warnings
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
/// Layout and sizing constants used throughout the application
|
||||
|
||||
/// Screen width breakpoints for responsive design
|
||||
class ScreenBreakpoints {
|
||||
/// Breakpoint for tablet devices (600px)
|
||||
|
||||
@@ -12,18 +12,9 @@ abstract class BaseMediaListDetailScreen<T extends StatefulWidget>
|
||||
extends State<T>
|
||||
with Refreshable, ItemUpdatable {
|
||||
// State properties - concrete implementations to avoid duplication
|
||||
List<PlexMetadata> _items = [];
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
List<PlexMetadata> get items => _items;
|
||||
set items(List<PlexMetadata> value) => _items = value;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
set isLoading(bool value) => _isLoading = value;
|
||||
|
||||
String? get errorMessage => _errorMessage;
|
||||
set errorMessage(String? value) => _errorMessage = value;
|
||||
List<PlexMetadata> items = [];
|
||||
bool isLoading = false;
|
||||
String? errorMessage;
|
||||
|
||||
@override
|
||||
PlexClient get client => context.clientSafe;
|
||||
|
||||
@@ -5,7 +5,6 @@ import '../models/plex_hub.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../models/plex_sort.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/grid_cross_axis_extent.dart';
|
||||
|
||||
@@ -315,7 +315,9 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
|
||||
return RadioListTile<String>(
|
||||
title: Text(_getGroupingLabel(grouping)),
|
||||
value: grouping,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: _selectedGrouping,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
@@ -325,6 +327,8 @@ class _LibraryBrowseTabState extends State<LibraryBrowseTab>
|
||||
final storage = await StorageService.getInstance();
|
||||
await storage.saveLibraryGrouping(widget.library.key, value);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pop(context);
|
||||
_loadItems();
|
||||
}
|
||||
|
||||
@@ -245,6 +245,8 @@ class _PlaylistDetailScreenState
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Set play queue in provider
|
||||
final playbackState = context.read<PlaybackStateProvider>();
|
||||
playbackState.setClient(client);
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../client/plex_client.dart';
|
||||
import '../models/plex_playlist.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/grid_size_calculator.dart';
|
||||
import '../utils/dialogs.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../mixins/refreshable.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import 'playlist_detail_screen.dart';
|
||||
|
||||
/// Screen to display all video playlists
|
||||
class PlaylistsScreen extends StatefulWidget {
|
||||
const PlaylistsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PlaylistsScreen> createState() => _PlaylistsScreenState();
|
||||
}
|
||||
|
||||
class _PlaylistsScreenState extends State<PlaylistsScreen> with Refreshable {
|
||||
PlexClient get client => context.clientSafe;
|
||||
|
||||
List<PlexPlaylist> _playlists = [];
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool? _filterSmart;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPlaylists();
|
||||
}
|
||||
|
||||
Future<void> _loadPlaylists() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final clientProvider = context.plexClient;
|
||||
final client = clientProvider.client;
|
||||
if (client == null) {
|
||||
throw Exception('No client available');
|
||||
}
|
||||
|
||||
final playlists = await client.getPlaylists(
|
||||
playlistType: 'video',
|
||||
smart: _filterSmart,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_playlists = playlists;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
appLogger.d('Loaded ${playlists.length} playlists');
|
||||
} catch (e) {
|
||||
appLogger.e('Failed to load playlists', error: e);
|
||||
setState(() {
|
||||
_errorMessage = 'Failed to load playlists: ${e.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleSmartFilter() {
|
||||
setState(() {
|
||||
if (_filterSmart == null) {
|
||||
_filterSmart = true; // Show only smart
|
||||
} else if (_filterSmart == true) {
|
||||
_filterSmart = false; // Show only regular
|
||||
} else {
|
||||
_filterSmart = null; // Show all
|
||||
}
|
||||
});
|
||||
_loadPlaylists();
|
||||
}
|
||||
|
||||
String _getFilterLabel() {
|
||||
if (_filterSmart == null) return 'All';
|
||||
if (_filterSmart == true) return 'Smart';
|
||||
return 'Regular';
|
||||
}
|
||||
|
||||
@override
|
||||
void refresh() {
|
||||
_loadPlaylists();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
CustomAppBar(
|
||||
title: Text(t.playlists.title),
|
||||
pinned: true,
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
label: Text(_getFilterLabel()),
|
||||
onPressed: _toggleSmartFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_errorMessage != null)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(_errorMessage!),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadPlaylists,
|
||||
child: Text(t.common.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_playlists.isEmpty && _isLoading)
|
||||
const SliverFillRemaining(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (_playlists.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text(t.playlists.noPlaylists)),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: GridSizeCalculator.getMaxCrossAxisExtent(
|
||||
context,
|
||||
context.watch<SettingsProvider>().libraryDensity,
|
||||
),
|
||||
childAspectRatio: 2 / 3.3,
|
||||
crossAxisSpacing: 0,
|
||||
mainAxisSpacing: 0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return _PlaylistCard(
|
||||
playlist: _playlists[index],
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
PlaylistDetailScreen(playlist: _playlists[index]),
|
||||
),
|
||||
).then((_) => _loadPlaylists()); // Refresh on return
|
||||
},
|
||||
onDeleted: _loadPlaylists,
|
||||
);
|
||||
}, childCount: _playlists.length),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget to display a single playlist card
|
||||
class _PlaylistCard extends StatelessWidget {
|
||||
final PlexPlaylist playlist;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onDeleted;
|
||||
|
||||
const _PlaylistCard({
|
||||
required this.playlist,
|
||||
required this.onTap,
|
||||
required this.onDeleted,
|
||||
});
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||
final confirmed = await showDeleteConfirmation(
|
||||
context,
|
||||
title: t.playlists.deleteConfirm,
|
||||
message: t.playlists.deleteMessage(name: playlist.title),
|
||||
);
|
||||
|
||||
if (confirmed == true && context.mounted) {
|
||||
final client = context.clientSafe;
|
||||
final success = await client.deletePlaylist(playlist.ratingKey);
|
||||
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.playlists.deleted)));
|
||||
onDeleted();
|
||||
} else {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(t.playlists.errorDeleting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = context.clientSafe;
|
||||
final imageUrl = playlist.displayImage != null
|
||||
? client.getThumbnailUrl(playlist.displayImage!)
|
||||
: null;
|
||||
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: const EdgeInsets.all(4),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: () => _showDeleteDialog(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Playlist image
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (imageUrl != null)
|
||||
Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildPlaceholder();
|
||||
},
|
||||
)
|
||||
else
|
||||
_buildPlaceholder(),
|
||||
// Smart playlist indicator
|
||||
if (playlist.smart)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.auto_awesome,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Playlist info
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
playlist.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.playlist_play,
|
||||
size: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
playlist.leafCount != null && playlist.leafCount! > 0
|
||||
? (playlist.leafCount == 1
|
||||
? t.playlists.oneItem
|
||||
: t.playlists.itemCount(
|
||||
count: playlist.leafCount!,
|
||||
))
|
||||
: t.playlists.emptyPlaylist,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlaceholder() {
|
||||
return Container(
|
||||
color: Colors.grey[850],
|
||||
child: const Center(
|
||||
child: Icon(Icons.playlist_play, size: 48, color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ Future<void> playCollectionOrPlaylist({
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(fetchedQueue, ratingKey);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(
|
||||
context,
|
||||
@@ -91,6 +93,8 @@ Future<void> playCollectionOrPlaylist({
|
||||
playbackState.setClient(client);
|
||||
await playbackState.setPlaybackFromPlayQueue(playQueue, ratingKey);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Navigate to first item
|
||||
await navigateToVideoPlayer(context, metadata: playQueue.items!.first);
|
||||
} catch (e) {
|
||||
|
||||
@@ -90,6 +90,7 @@ Future<void> handleShufflePlay(
|
||||
episodes.shuffle();
|
||||
|
||||
// Store shuffle queue in provider
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
playbackState.setShuffleQueue(episodes, metadata.ratingKey);
|
||||
|
||||
// Navigate to first episode
|
||||
|
||||
@@ -34,7 +34,9 @@ class EmptyStateWidget extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
@@ -42,7 +44,9 @@ 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.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (onAction != null && actionLabel != null) ...[
|
||||
|
||||
@@ -89,7 +89,9 @@ class FolderTreeItem extends StatelessWidget {
|
||||
size: 20,
|
||||
color: isFolder
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
@@ -115,7 +117,7 @@ class FolderTreeItem extends StatelessWidget {
|
||||
fontSize: 12,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.6),
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -607,13 +607,11 @@ Widget _buildPosterImage(BuildContext context, dynamic item) {
|
||||
IconData fallbackIcon = Icons.movie;
|
||||
|
||||
if (item is PlexPlaylist) {
|
||||
posterUrl = (item as PlexPlaylist).displayImage;
|
||||
posterUrl = item.displayImage;
|
||||
fallbackIcon = Icons.playlist_play;
|
||||
} else if (item is PlexMetadata) {
|
||||
final useSeasonPoster = context.watch<SettingsProvider>().useSeasonPoster;
|
||||
posterUrl = (item as PlexMetadata).posterThumb(
|
||||
useSeasonPoster: useSeasonPoster,
|
||||
);
|
||||
posterUrl = item.posterThumb(useSeasonPoster: useSeasonPoster);
|
||||
}
|
||||
|
||||
if (posterUrl != null) {
|
||||
|
||||
@@ -608,6 +608,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
final itemUri = await client.buildMetadataUri(metadata.ratingKey);
|
||||
appLogger.d('Built URI for $itemType: $itemUri');
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (result == '_create_new') {
|
||||
// Create new playlist flow
|
||||
final playlistName = await showDialog<String>(
|
||||
@@ -628,6 +630,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
uri: itemUri,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (context.mounted) {
|
||||
if (newPlaylist != null) {
|
||||
appLogger.d('Successfully created playlist: ${newPlaylist.title}');
|
||||
@@ -651,6 +655,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
uri: itemUri,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
appLogger.d('Successfully added item(s) to playlist $result');
|
||||
@@ -819,6 +825,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
type: collectionType,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (context.mounted) {
|
||||
if (newCollectionId != null) {
|
||||
appLogger.d(
|
||||
@@ -834,6 +842,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
uri: itemUri,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (addSuccess) {
|
||||
appLogger.d('Successfully added item to new collection');
|
||||
ScaffoldMessenger.of(
|
||||
@@ -862,6 +872,8 @@ class _MediaContextMenuState extends State<MediaContextMenu> {
|
||||
uri: itemUri,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
appLogger.d('Successfully added item(s) to collection $result');
|
||||
|
||||
Reference in New Issue
Block a user