refactor: unify library management

This commit is contained in:
edde746
2025-11-04 07:44:36 +01:00
parent aa9dd045de
commit 73568ee3ae
2 changed files with 218 additions and 246 deletions
+218 -140
View File
@@ -12,7 +12,6 @@ import '../utils/provider_extensions.dart';
import '../widgets/media_card.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/app_bar_back_button.dart';
import '../widgets/context_menu_wrapper.dart';
import '../services/storage_service.dart';
import '../services/settings_service.dart';
import '../mixins/refreshable.dart';
@@ -43,7 +42,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
PlexSort? _selectedSort;
bool _isSortDescending = false;
bool _isInitialLoad = true;
bool _isReorderMode = false;
@override
void initState() {
@@ -406,44 +404,35 @@ class _LibrariesScreenState extends State<LibrariesScreen>
}
}
Future<void> _hideLibrary(PlexLibrary library) async {
// Hide library using provider
Future<void> _toggleLibraryVisibility(PlexLibrary library) async {
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(
context,
listen: false,
);
await hiddenLibrariesProvider.hideLibrary(library.key);
final isHidden = hiddenLibrariesProvider.hiddenLibraryKeys.contains(library.key);
// Reload libraries to update the visible list
await _loadLibraries();
if (isHidden) {
await hiddenLibrariesProvider.unhideLibrary(library.key);
} else {
// Check if we're hiding the currently selected library
final isCurrentlySelected = _selectedLibraryKey == library.key;
// Show snackbar with undo option
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Hidden "${library.title}"'),
action: SnackBarAction(
label: 'Undo',
onPressed: () => _unhideLibrary(library.key),
),
duration: const Duration(seconds: 4),
),
);
await hiddenLibrariesProvider.hideLibrary(library.key);
// If we just hid the selected library, select the first visible one
if (isCurrentlySelected) {
// Compute visible libraries after hiding
final visibleLibraries = _allLibraries
.where((lib) => !hiddenLibrariesProvider.hiddenLibraryKeys.contains(lib.key))
.toList();
if (visibleLibraries.isNotEmpty) {
_loadLibraryContent(visibleLibraries.first.key);
}
}
}
}
Future<void> _unhideLibrary(String libraryKey) async {
// Unhide library using provider
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(
context,
listen: false,
);
await hiddenLibrariesProvider.unhideLibrary(libraryKey);
// Reload libraries to update the visible list
await _loadLibraries();
}
void _showFiltersBottomSheet() {
showModalBottomSheet(
context: context,
@@ -483,6 +472,29 @@ class _LibrariesScreenState extends State<LibrariesScreen>
);
}
void _showLibraryManagementSheet() {
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(
context,
listen: false,
);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => _LibraryManagementSheet(
allLibraries: List.from(_allLibraries),
hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys,
onReorder: (reorderedLibraries) {
setState(() {
_allLibraries = reorderedLibraries;
});
_saveLibraryOrder();
},
onToggleVisibility: _toggleLibraryVisibility,
),
);
}
@override
Widget build(BuildContext context) {
// Watch for hidden libraries changes to trigger rebuild
@@ -508,15 +520,8 @@ class _LibrariesScreenState extends State<LibrariesScreen>
actions: [
if (_allLibraries.isNotEmpty)
IconButton(
icon: Icon(
_isReorderMode ? Icons.check : Icons.edit,
semanticLabel: _isReorderMode ? 'Done' : 'Reorder',
),
onPressed: () {
setState(() {
_isReorderMode = !_isReorderMode;
});
},
icon: const Icon(Icons.edit, semanticLabel: 'Manage Libraries'),
onPressed: _showLibraryManagementSheet,
),
if (_sortOptions.isNotEmpty)
IconButton(
@@ -593,66 +598,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
horizontal: 16,
vertical: 8,
),
child: _isReorderMode
? ReorderableListView.builder(
scrollDirection: Axis.horizontal,
onReorder: _reorderLibraries,
itemCount: _allLibraries.length,
proxyDecorator: (child, index, animation) {
return Material(
elevation: 4,
borderRadius: BorderRadius.circular(8),
child: child,
);
},
itemBuilder: (context, index) {
final library = _allLibraries[index];
final isSelected = library.key == _selectedLibraryKey;
final isHidden = hiddenKeys.contains(library.key);
final t = tokens(context);
return Container(
key: ValueKey(library.key),
margin: const EdgeInsets.only(right: 8),
child: Chip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.drag_handle,
size: 16,
color: isSelected ? t.bg : t.text,
),
const SizedBox(width: 6),
Icon(
_getLibraryIcon(library.type),
size: 16,
color: isSelected ? t.bg : t.text,
),
const SizedBox(width: 6),
Text(library.title),
if (isHidden) ...[
const SizedBox(width: 6),
Icon(
Icons.visibility_off,
size: 16,
color: isSelected ? t.bg : t.text,
),
],
],
),
backgroundColor: isSelected ? t.text : t.surface,
side: BorderSide(color: t.outline),
labelStyle: TextStyle(
color: isSelected ? t.bg : t.text,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
),
);
},
)
: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(visibleLibraries.length, (index) {
@@ -661,49 +607,35 @@ class _LibrariesScreenState extends State<LibrariesScreen>
final t = tokens(context);
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ContextMenuWrapper(
menuItems: [
ContextMenuItem(
value: 'hide',
icon: Icons.visibility_off,
label: 'Hide "${library.title}"',
),
],
onMenuItemSelected: (value) {
if (value == 'hide') {
_hideLibrary(library);
child: ChoiceChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getLibraryIcon(library.type),
size: 16,
color: isSelected ? t.bg : t.text,
),
const SizedBox(width: 6),
Text(library.title),
],
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
_loadLibraryContent(library.key);
}
},
child: ChoiceChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getLibraryIcon(library.type),
size: 16,
color: isSelected ? t.bg : t.text,
),
const SizedBox(width: 6),
Text(library.title),
],
),
selected: isSelected,
onSelected: (selected) {
if (selected) {
_loadLibraryContent(library.key);
}
},
backgroundColor: t.surface,
selectedColor: t.text,
side: BorderSide(color: t.outline),
labelStyle: TextStyle(
color: isSelected ? t.bg : t.text,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
showCheckmark: false,
backgroundColor: t.surface,
selectedColor: t.text,
side: BorderSide(color: t.outline),
labelStyle: TextStyle(
color: isSelected ? t.bg : t.text,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
showCheckmark: false,
),
);
}),
@@ -1292,3 +1224,149 @@ class _SortBottomSheetState extends State<_SortBottomSheet> {
);
}
}
class _LibraryManagementSheet extends StatefulWidget {
final List<PlexLibrary> allLibraries;
final Set<String> hiddenLibraryKeys;
final Function(List<PlexLibrary>) onReorder;
final Function(PlexLibrary) onToggleVisibility;
const _LibraryManagementSheet({
required this.allLibraries,
required this.hiddenLibraryKeys,
required this.onReorder,
required this.onToggleVisibility,
});
@override
State<_LibraryManagementSheet> createState() => _LibraryManagementSheetState();
}
class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
late List<PlexLibrary> _tempLibraries;
@override
void initState() {
super.initState();
_tempLibraries = List.from(widget.allLibraries);
}
void _reorderLibraries(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final library = _tempLibraries.removeAt(oldIndex);
_tempLibraries.insert(newIndex, library);
});
// Apply immediately
widget.onReorder(_tempLibraries);
}
IconData _getLibraryIcon(String type) {
switch (type.toLowerCase()) {
case 'movie':
return Icons.movie;
case 'show':
return Icons.tv;
case 'artist':
return Icons.music_note;
case 'photo':
return Icons.photo;
default:
return Icons.folder;
}
}
@override
Widget build(BuildContext context) {
// Watch provider to rebuild when hidden libraries change
final hiddenLibrariesProvider = context.watch<HiddenLibrariesProvider>();
final hiddenLibraryKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
return DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Row(
children: [
const Icon(Icons.edit),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Manage Libraries',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Reorderable library list
Expanded(
child: ReorderableListView.builder(
scrollController: scrollController,
onReorder: _reorderLibraries,
itemCount: _tempLibraries.length,
padding: const EdgeInsets.symmetric(vertical: 8),
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final library = _tempLibraries[index];
final isHidden = hiddenLibraryKeys.contains(library.key);
return ReorderableDragStartListener(
key: ValueKey(library.key),
index: index,
child: Opacity(
opacity: isHidden ? 0.5 : 1.0,
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.drag_indicator,
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.5),
),
const SizedBox(width: 8),
Icon(_getLibraryIcon(library.type)),
],
),
title: Text(library.title),
trailing: IconButton(
icon: Icon(
isHidden ? Icons.visibility_off : Icons.visibility,
),
onPressed: () => widget.onToggleVisibility(library),
tooltip: isHidden ? 'Show library' : 'Hide library',
),
),
),
);
},
),
),
],
);
},
);
}
}
-106
View File
@@ -3,11 +3,8 @@ import 'package:provider/provider.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import '../providers/theme_provider.dart';
import '../providers/settings_provider.dart';
import '../providers/plex_client_provider.dart';
import '../providers/hidden_libraries_provider.dart';
import '../services/settings_service.dart' as settings;
import '../services/keyboard_shortcuts_service.dart';
import '../models/plex_library.dart';
import '../widgets/desktop_app_bar.dart';
import '../widgets/hotkey_recorder_widget.dart';
import 'about_screen.dart';
@@ -46,21 +43,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
}
Future<void> _unhideLibrary(String libraryKey) async {
// Unhide library using provider
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(
context,
listen: false,
);
await hiddenLibrariesProvider.unhideLibrary(libraryKey);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Library shown')),
);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
@@ -77,8 +59,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
delegate: SliverChildListDelegate([
_buildAppearanceSection(),
const SizedBox(height: 24),
_buildLibraryManagementSection(),
const SizedBox(height: 24),
_buildVideoPlaybackSection(),
const SizedBox(height: 24),
_buildKeyboardShortcutsSection(),
@@ -151,92 +131,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildLibraryManagementSection() {
// Watch for hidden libraries changes to trigger rebuild
final hiddenLibrariesProvider = context.watch<HiddenLibrariesProvider>();
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Library Management',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
if (hiddenKeys.isEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Text(
'No hidden libraries',
style: TextStyle(color: Colors.grey[600]),
),
)
else
// Use FutureBuilder to fetch library details for hidden keys
FutureBuilder<List<PlexLibrary>>(
future: _fetchHiddenLibraries(hiddenKeys),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError || !snapshot.hasData) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Text(
'Error loading hidden libraries',
style: TextStyle(color: Colors.grey[600]),
),
);
}
final hiddenLibraries = snapshot.data!;
return Column(
children: hiddenLibraries.map((library) {
return ListTile(
leading: const Icon(Icons.visibility_off),
title: Text(library.title),
subtitle: Text('${library.type} library'),
trailing: TextButton(
onPressed: () => _unhideLibrary(library.key),
child: const Text('Show'),
),
);
}).toList(),
);
},
),
],
),
);
}
Future<List<PlexLibrary>> _fetchHiddenLibraries(Set<String> hiddenKeys) async {
final clientProvider = Provider.of<PlexClientProvider>(
context,
listen: false,
);
final client = clientProvider.client;
if (client == null) {
return [];
}
final allLibraries = await client.getLibraries();
return allLibraries
.where((lib) => hiddenKeys.contains(lib.key))
.toList();
}
Widget _buildVideoPlaybackSection() {
return Card(
child: Column(