Files
plezy/lib/screens/settings_screen.dart
T
2025-11-04 06:45:16 +01:00

774 lines
25 KiB
Dart

import 'package:flutter/material.dart';
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';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late settings.SettingsService _settingsService;
late KeyboardShortcutsService _keyboardService;
bool _isLoading = true;
bool _enableDebugLogging = false;
bool _enableHardwareDecoding = true;
int _bufferSize = 128;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
_settingsService = await settings.SettingsService.getInstance();
_keyboardService = await KeyboardShortcutsService.getInstance();
setState(() {
_enableDebugLogging = _settingsService.getEnableDebugLogging();
_enableHardwareDecoding = _settingsService.getEnableHardwareDecoding();
_bufferSize = _settingsService.getBufferSize();
_isLoading = false;
});
}
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) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
body: CustomScrollView(
slivers: [
const CustomAppBar(title: Text('Settings'), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildAppearanceSection(),
const SizedBox(height: 24),
_buildLibraryManagementSection(),
const SizedBox(height: 24),
_buildVideoPlaybackSection(),
const SizedBox(height: 24),
_buildKeyboardShortcutsSection(),
const SizedBox(height: 24),
_buildAdvancedSection(),
const SizedBox(height: 24),
_buildAboutSection(),
const SizedBox(height: 24),
]),
),
),
],
),
);
}
Widget _buildAppearanceSection() {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Appearance',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return ListTile(
leading: Icon(themeProvider.themeModeIcon),
title: const Text('Theme'),
subtitle: Text(themeProvider.themeModeDisplayName),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeDialog(themeProvider),
);
},
),
Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return ListTile(
leading: const Icon(Icons.grid_view),
title: const Text('Library Density'),
subtitle: Text(settingsProvider.libraryDensityDisplayName),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLibraryDensityDialog(),
);
},
),
Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return SwitchListTile(
secondary: const Icon(Icons.image),
title: const Text('Use Season Posters'),
subtitle: const Text(
'Show season poster instead of series poster for episodes',
),
value: settingsProvider.useSeasonPoster,
onChanged: (value) async {
await settingsProvider.setUseSeasonPoster(value);
},
);
},
),
],
),
);
}
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Video Playback',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
SwitchListTile(
secondary: const Icon(Icons.hardware),
title: const Text('Hardware Decoding'),
subtitle: const Text('Use hardware acceleration when available'),
value: _enableHardwareDecoding,
onChanged: (value) async {
setState(() {
_enableHardwareDecoding = value;
});
await _settingsService.setEnableHardwareDecoding(value);
},
),
ListTile(
leading: const Icon(Icons.memory),
title: const Text('Buffer Size'),
subtitle: Text('${_bufferSize}MB'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showBufferSizeDialog(),
),
],
),
);
}
Widget _buildKeyboardShortcutsSection() {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Keyboard Shortcuts',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
ListTile(
leading: const Icon(Icons.keyboard),
title: const Text('Video Player Controls'),
subtitle: const Text('Customize keyboard shortcuts'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showKeyboardShortcutsDialog(),
),
],
),
);
}
Widget _buildAdvancedSection() {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Advanced',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
SwitchListTile(
secondary: const Icon(Icons.bug_report),
title: const Text('Debug Logging'),
subtitle: const Text('Enable detailed logging for troubleshooting'),
value: _enableDebugLogging,
onChanged: (value) async {
setState(() {
_enableDebugLogging = value;
});
await _settingsService.setEnableDebugLogging(value);
},
),
ListTile(
leading: const Icon(Icons.cleaning_services),
title: const Text('Clear Cache'),
subtitle: const Text('Free up storage space'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showClearCacheDialog(),
),
ListTile(
leading: const Icon(Icons.restore),
title: const Text('Reset Settings'),
subtitle: const Text('Reset all settings to defaults'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showResetSettingsDialog(),
),
],
),
);
}
Widget _buildAboutSection() {
return Card(
child: ListTile(
leading: const Icon(Icons.info),
title: const Text('About'),
subtitle: const Text('App information and licenses'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AboutScreen()),
);
},
),
);
}
void _showThemeDialog(ThemeProvider themeProvider) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Theme'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
themeProvider.themeMode == settings.ThemeMode.system
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('System'),
subtitle: const Text('Follow system settings'),
onTap: () {
themeProvider.setThemeMode(settings.ThemeMode.system);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(
themeProvider.themeMode == settings.ThemeMode.light
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('Light'),
onTap: () {
themeProvider.setThemeMode(settings.ThemeMode.light);
Navigator.pop(context);
},
),
ListTile(
leading: Icon(
themeProvider.themeMode == settings.ThemeMode.dark
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('Dark'),
onTap: () {
themeProvider.setThemeMode(settings.ThemeMode.dark);
Navigator.pop(context);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
);
},
);
}
void _showBufferSizeDialog() {
final options = [64, 128, 256, 512, 1024];
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Buffer Size'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: options.map((size) {
return ListTile(
leading: Icon(
_bufferSize == size
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: Text('${size}MB'),
onTap: () {
setState(() {
_bufferSize = size;
_settingsService.setBufferSize(size);
});
Navigator.pop(context);
},
);
}).toList(),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
);
},
);
}
void _showKeyboardShortcutsDialog() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
_KeyboardShortcutsScreen(keyboardService: _keyboardService),
),
);
}
void _showClearCacheDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Clear Cache'),
content: const Text(
'This will clear all cached images and data. The app may take longer to load content after clearing the cache.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
await _settingsService.clearCache();
if (mounted) {
navigator.pop();
messenger.showSnackBar(
const SnackBar(content: Text('Cache cleared successfully')),
);
}
},
child: const Text('Clear'),
),
],
);
},
);
}
void _showResetSettingsDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Reset Settings'),
content: const Text(
'This will reset all settings to their default values. This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
await _settingsService.resetAllSettings();
await _keyboardService.resetToDefaults();
if (mounted) {
navigator.pop();
messenger.showSnackBar(
const SnackBar(
content: Text('Settings reset successfully'),
),
);
// Reload settings
_loadSettings();
}
},
child: const Text('Reset'),
),
],
);
},
);
}
void _showLibraryDensityDialog() {
final settingsProvider = context.read<SettingsProvider>();
showDialog(
context: context,
builder: (BuildContext context) {
return Consumer<SettingsProvider>(
builder: (context, provider, child) {
return AlertDialog(
title: const Text('Library Density'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
provider.libraryDensity == settings.LibraryDensity.compact
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('Compact'),
subtitle: const Text('Smaller cards, more items visible'),
onTap: () async {
await settingsProvider.setLibraryDensity(
settings.LibraryDensity.compact,
);
if (context.mounted) Navigator.pop(context);
},
),
ListTile(
leading: Icon(
provider.libraryDensity == settings.LibraryDensity.normal
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('Normal'),
subtitle: const Text('Default size'),
onTap: () async {
await settingsProvider.setLibraryDensity(
settings.LibraryDensity.normal,
);
if (context.mounted) Navigator.pop(context);
},
),
ListTile(
leading: Icon(
provider.libraryDensity ==
settings.LibraryDensity.comfortable
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: const Text('Comfortable'),
subtitle: const Text('Larger cards, fewer items visible'),
onTap: () async {
await settingsProvider.setLibraryDensity(
settings.LibraryDensity.comfortable,
);
if (context.mounted) Navigator.pop(context);
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
);
},
);
},
);
}
}
class _KeyboardShortcutsScreen extends StatefulWidget {
final KeyboardShortcutsService keyboardService;
const _KeyboardShortcutsScreen({required this.keyboardService});
@override
State<_KeyboardShortcutsScreen> createState() =>
_KeyboardShortcutsScreenState();
}
class _KeyboardShortcutsScreenState extends State<_KeyboardShortcutsScreen> {
Map<String, HotKey> _hotkeys = {};
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadHotkeys();
}
Future<void> _loadHotkeys() async {
await widget.keyboardService.refreshFromStorage();
setState(() {
_hotkeys = widget.keyboardService.hotkeys;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(
title: const Text('Keyboard Shortcuts'),
pinned: true,
actions: [
TextButton(
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
await widget.keyboardService.resetToDefaults();
await _loadHotkeys();
if (mounted) {
messenger.showSnackBar(
const SnackBar(
content: Text('Shortcuts reset to defaults'),
),
);
}
},
child: const Text('Reset'),
),
],
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final actions = _hotkeys.keys.toList();
final action = actions[index];
final hotkey = _hotkeys[action]!;
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(
widget.keyboardService.getActionDisplayName(action),
),
subtitle: Text(action),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(6),
),
child: Text(
widget.keyboardService.formatHotkey(hotkey),
style: const TextStyle(fontFamily: 'monospace'),
),
),
onTap: () => _editHotkey(action, hotkey),
),
);
}, childCount: _hotkeys.length),
),
),
],
),
);
}
void _editHotkey(String action, HotKey currentHotkey) {
showDialog(
context: context,
builder: (BuildContext context) {
return HotKeyRecorderWidget(
actionName: widget.keyboardService.getActionDisplayName(action),
currentHotKey: currentHotkey,
onHotKeyRecorded: (newHotkey) async {
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
// Check for conflicts
final existingAction = widget.keyboardService.getActionForHotkey(
newHotkey,
);
if (existingAction != null && existingAction != action) {
navigator.pop();
messenger.showSnackBar(
SnackBar(
content: Text(
'Shortcut already assigned to ${widget.keyboardService.getActionDisplayName(existingAction)}',
),
),
);
return;
}
// Save the new hotkey
await widget.keyboardService.setHotkey(action, newHotkey);
if (mounted) {
// Update UI directly instead of reloading from storage
setState(() {
_hotkeys[action] = newHotkey;
});
navigator.pop();
messenger.showSnackBar(
SnackBar(
content: Text(
'Shortcut updated for ${widget.keyboardService.getActionDisplayName(action)}',
),
),
);
}
},
onCancel: () => Navigator.pop(context),
);
},
);
}
}