Provider Icons: - Add provider icon overlays to series detail screen (grid/list) - Add provider icon overlays to library playlists (grid/list) - Add ProviderIconOverlay widget and provider manifest model Performance Optimizations: - Fix empty setState in SyncService listener - only rebuild on sync complete - Add cached sorting for albums, artists, authors (avoid O(n log n) per rebuild) - Add cached track sort keys for LetterScrollbar - Fix image cache size mismatches (author 128px, audiobook list 128px, series 150px) - Simplify provider toggle setState to avoid double rebuild - Remove redundant text Hero animations from card widgets - Defer color extraction from card tap to detail screen initState - Increase fallback image debounce from 200ms to 400ms Other Changes: - Update screenshots - Add new localizations - Various detail screen improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1143 lines
45 KiB
Dart
1143 lines
45 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
import '../providers/locale_provider.dart';
|
|
import '../providers/music_assistant_provider.dart';
|
|
import '../services/music_assistant_api.dart';
|
|
import '../services/settings_service.dart';
|
|
import '../theme/theme_provider.dart';
|
|
import '../theme/app_theme.dart';
|
|
import '../widgets/global_player_overlay.dart';
|
|
import 'debug_log_screen.dart';
|
|
import 'login_screen.dart';
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
final _lastFmApiKeyController = TextEditingController();
|
|
final _audioDbApiKeyController = TextEditingController();
|
|
// Main rows (default on)
|
|
bool _showRecentAlbums = true;
|
|
bool _showDiscoverArtists = true;
|
|
bool _showDiscoverAlbums = true;
|
|
// Favorites rows (default off)
|
|
bool _showFavoriteAlbums = false;
|
|
bool _showFavoriteArtists = false;
|
|
bool _showFavoriteTracks = false;
|
|
bool _showFavoritePlaylists = false;
|
|
bool _showFavoriteRadioStations = false;
|
|
bool _showFavoritePodcasts = false;
|
|
// Audiobook home rows (default off)
|
|
bool _showContinueListeningAudiobooks = false;
|
|
bool _showDiscoverAudiobooks = false;
|
|
bool _showDiscoverSeries = false;
|
|
// Home row order
|
|
List<String> _homeRowOrder = List.from(SettingsService.defaultHomeRowOrder);
|
|
// Player settings
|
|
bool _preferLocalPlayer = false;
|
|
bool _smartSortPlayers = false;
|
|
bool _disableAutoSwitch = false;
|
|
bool _volumePrecisionMode = true;
|
|
// Hint settings
|
|
bool _showHints = true;
|
|
// Display settings
|
|
bool _showProviderIcons = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSettings();
|
|
}
|
|
|
|
Future<void> _loadSettings() async {
|
|
final lastFmKey = await SettingsService.getLastFmApiKey();
|
|
if (lastFmKey != null) {
|
|
_lastFmApiKeyController.text = lastFmKey;
|
|
}
|
|
|
|
final audioDbKey = await SettingsService.getTheAudioDbApiKey();
|
|
if (audioDbKey != null) {
|
|
_audioDbApiKeyController.text = audioDbKey;
|
|
}
|
|
|
|
// Load home screen settings
|
|
final showRecent = await SettingsService.getShowRecentAlbums();
|
|
final showDiscArtists = await SettingsService.getShowDiscoverArtists();
|
|
final showDiscAlbums = await SettingsService.getShowDiscoverAlbums();
|
|
final showFavAlbums = await SettingsService.getShowFavoriteAlbums();
|
|
final showFavArtists = await SettingsService.getShowFavoriteArtists();
|
|
final showFavTracks = await SettingsService.getShowFavoriteTracks();
|
|
final showFavPlaylists = await SettingsService.getShowFavoritePlaylists();
|
|
final showFavRadio = await SettingsService.getShowFavoriteRadioStations();
|
|
final showFavPodcasts = await SettingsService.getShowFavoritePodcasts();
|
|
|
|
// Load audiobook home row settings
|
|
final showContinueAudiobooks = await SettingsService.getShowContinueListeningAudiobooks();
|
|
final showDiscAudiobooks = await SettingsService.getShowDiscoverAudiobooks();
|
|
final showDiscSeries = await SettingsService.getShowDiscoverSeries();
|
|
|
|
// Load home row order
|
|
final rowOrder = await SettingsService.getHomeRowOrder();
|
|
|
|
// Load player settings
|
|
final preferLocal = await SettingsService.getPreferLocalPlayer();
|
|
final smartSort = await SettingsService.getSmartSortPlayers();
|
|
final disableAutoSwitch = await SettingsService.getDisableAutoSwitch();
|
|
final volumePrecision = await SettingsService.getVolumePrecisionMode();
|
|
|
|
// Load hint settings
|
|
final showHints = await SettingsService.getShowHints();
|
|
|
|
// Load display settings
|
|
final showProviderIcons = await SettingsService.getShowProviderIcons();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_showRecentAlbums = showRecent;
|
|
_showDiscoverArtists = showDiscArtists;
|
|
_showDiscoverAlbums = showDiscAlbums;
|
|
_showFavoriteAlbums = showFavAlbums;
|
|
_showFavoriteArtists = showFavArtists;
|
|
_showFavoriteTracks = showFavTracks;
|
|
_showFavoritePlaylists = showFavPlaylists;
|
|
_showFavoriteRadioStations = showFavRadio;
|
|
_showFavoritePodcasts = showFavPodcasts;
|
|
_showContinueListeningAudiobooks = showContinueAudiobooks;
|
|
_showDiscoverAudiobooks = showDiscAudiobooks;
|
|
_showDiscoverSeries = showDiscSeries;
|
|
_homeRowOrder = rowOrder;
|
|
_preferLocalPlayer = preferLocal;
|
|
_smartSortPlayers = smartSort;
|
|
_disableAutoSwitch = disableAutoSwitch;
|
|
_volumePrecisionMode = volumePrecision;
|
|
_showHints = showHints;
|
|
_showProviderIcons = showProviderIcons;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_lastFmApiKeyController.dispose();
|
|
_audioDbApiKeyController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// Helper to get row display info
|
|
Map<String, String> _getRowInfo(String rowId) {
|
|
final s = S.of(context)!;
|
|
switch (rowId) {
|
|
case 'recent-albums':
|
|
return {'title': s.recentlyPlayed, 'subtitle': s.showRecentlyPlayedAlbums};
|
|
case 'discover-artists':
|
|
return {'title': s.discoverArtists, 'subtitle': s.showRandomArtists};
|
|
case 'discover-albums':
|
|
return {'title': s.discoverAlbums, 'subtitle': s.showRandomAlbums};
|
|
case 'continue-listening':
|
|
return {'title': s.continueListening, 'subtitle': s.showAudiobooksInProgress};
|
|
case 'discover-audiobooks':
|
|
return {'title': s.discoverAudiobooks, 'subtitle': s.showRandomAudiobooks};
|
|
case 'discover-series':
|
|
return {'title': s.discoverSeries, 'subtitle': s.showRandomSeries};
|
|
case 'favorite-albums':
|
|
return {'title': s.favoriteAlbums, 'subtitle': s.showFavoriteAlbums};
|
|
case 'favorite-artists':
|
|
return {'title': s.favoriteArtists, 'subtitle': s.showFavoriteArtists};
|
|
case 'favorite-tracks':
|
|
return {'title': s.favoriteTracks, 'subtitle': s.showFavoriteTracks};
|
|
case 'favorite-playlists':
|
|
return {'title': s.favoritePlaylists, 'subtitle': s.showFavoritePlaylists};
|
|
case 'favorite-radio-stations':
|
|
return {'title': s.favoriteRadioStations, 'subtitle': s.showFavoriteRadioStations};
|
|
case 'favorite-podcasts':
|
|
return {'title': s.favoritePodcasts, 'subtitle': s.showFavoritePodcasts};
|
|
default:
|
|
return {'title': rowId, 'subtitle': ''};
|
|
}
|
|
}
|
|
|
|
// Helper to get row enabled state
|
|
bool _getRowEnabled(String rowId) {
|
|
switch (rowId) {
|
|
case 'recent-albums':
|
|
return _showRecentAlbums;
|
|
case 'discover-artists':
|
|
return _showDiscoverArtists;
|
|
case 'discover-albums':
|
|
return _showDiscoverAlbums;
|
|
case 'continue-listening':
|
|
return _showContinueListeningAudiobooks;
|
|
case 'discover-audiobooks':
|
|
return _showDiscoverAudiobooks;
|
|
case 'discover-series':
|
|
return _showDiscoverSeries;
|
|
case 'favorite-albums':
|
|
return _showFavoriteAlbums;
|
|
case 'favorite-artists':
|
|
return _showFavoriteArtists;
|
|
case 'favorite-tracks':
|
|
return _showFavoriteTracks;
|
|
case 'favorite-playlists':
|
|
return _showFavoritePlaylists;
|
|
case 'favorite-radio-stations':
|
|
return _showFavoriteRadioStations;
|
|
case 'favorite-podcasts':
|
|
return _showFavoritePodcasts;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Helper to set row enabled state
|
|
void _setRowEnabled(String rowId, bool value) {
|
|
setState(() {
|
|
switch (rowId) {
|
|
case 'recent-albums':
|
|
_showRecentAlbums = value;
|
|
SettingsService.setShowRecentAlbums(value);
|
|
break;
|
|
case 'discover-artists':
|
|
_showDiscoverArtists = value;
|
|
SettingsService.setShowDiscoverArtists(value);
|
|
break;
|
|
case 'discover-albums':
|
|
_showDiscoverAlbums = value;
|
|
SettingsService.setShowDiscoverAlbums(value);
|
|
break;
|
|
case 'continue-listening':
|
|
_showContinueListeningAudiobooks = value;
|
|
SettingsService.setShowContinueListeningAudiobooks(value);
|
|
break;
|
|
case 'discover-audiobooks':
|
|
_showDiscoverAudiobooks = value;
|
|
SettingsService.setShowDiscoverAudiobooks(value);
|
|
break;
|
|
case 'discover-series':
|
|
_showDiscoverSeries = value;
|
|
SettingsService.setShowDiscoverSeries(value);
|
|
break;
|
|
case 'favorite-albums':
|
|
_showFavoriteAlbums = value;
|
|
SettingsService.setShowFavoriteAlbums(value);
|
|
break;
|
|
case 'favorite-artists':
|
|
_showFavoriteArtists = value;
|
|
SettingsService.setShowFavoriteArtists(value);
|
|
break;
|
|
case 'favorite-tracks':
|
|
_showFavoriteTracks = value;
|
|
SettingsService.setShowFavoriteTracks(value);
|
|
break;
|
|
case 'favorite-playlists':
|
|
_showFavoritePlaylists = value;
|
|
SettingsService.setShowFavoritePlaylists(value);
|
|
break;
|
|
case 'favorite-radio-stations':
|
|
_showFavoriteRadioStations = value;
|
|
SettingsService.setShowFavoriteRadioStations(value);
|
|
break;
|
|
case 'favorite-podcasts':
|
|
_showFavoritePodcasts = value;
|
|
SettingsService.setShowFavoritePodcasts(value);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _disconnect() async {
|
|
final provider = context.read<MusicAssistantProvider>();
|
|
await provider.disconnect();
|
|
|
|
// Clear saved server URL so login screen shows
|
|
await SettingsService.clearServerUrl();
|
|
|
|
if (mounted) {
|
|
// Navigate to login screen and clear the navigation stack
|
|
Navigator.of(context).pushAndRemoveUntil(
|
|
MaterialPageRoute(builder: (context) => const LoginScreen()),
|
|
(route) => false,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = context.watch<MusicAssistantProvider>();
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return Scaffold(
|
|
backgroundColor: colorScheme.background,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
onPressed: () => Navigator.pop(context),
|
|
color: colorScheme.onBackground,
|
|
),
|
|
title: Text(
|
|
S.of(context)!.settings,
|
|
style: textTheme.titleLarge?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
),
|
|
centerTitle: true,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.bug_report_rounded),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DebugLogScreen(),
|
|
),
|
|
);
|
|
},
|
|
color: colorScheme.onBackground,
|
|
tooltip: S.of(context)!.debugLogs,
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// Logo - same size as login screen (50% of screen width)
|
|
// Use color filter to make logo dark in light theme
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0, bottom: 48.0),
|
|
child: Theme.of(context).brightness == Brightness.light
|
|
? ColorFiltered(
|
|
colorFilter: const ColorFilter.mode(
|
|
Color(0xFF1a1a1a), // Dark color for light theme
|
|
BlendMode.srcIn,
|
|
),
|
|
child: Image.asset(
|
|
'assets/images/ensemble_icon_transparent.png',
|
|
width: MediaQuery.of(context).size.width * 0.5,
|
|
fit: BoxFit.contain,
|
|
),
|
|
)
|
|
: Image.asset(
|
|
'assets/images/ensemble_icon_transparent.png',
|
|
width: MediaQuery.of(context).size.width * 0.5,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
|
|
// Connection status box - centered with border radius like theme boxes
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_getStatusIcon(provider.connectionState),
|
|
color: _getStatusColor(provider.connectionState, colorScheme),
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_getStatusText(provider.connectionState),
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: _getStatusColor(provider.connectionState, colorScheme),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Disconnect button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: FilledButton.tonalIcon(
|
|
onPressed: _disconnect,
|
|
icon: const Icon(Icons.logout_rounded),
|
|
label: Text(
|
|
S.of(context)!.disconnect,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colorScheme.errorContainer.withOpacity(0.4),
|
|
foregroundColor: colorScheme.error,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Theme section
|
|
Text(
|
|
S.of(context)!.theme,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.5),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
S.of(context)!.themeMode,
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Consumer<ThemeProvider>(
|
|
builder: (context, themeProvider, _) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: SegmentedButton<ThemeMode>(
|
|
segments: [
|
|
ButtonSegment<ThemeMode>(
|
|
value: ThemeMode.light,
|
|
label: Text(S.of(context)!.light),
|
|
icon: const Icon(Icons.light_mode_rounded),
|
|
),
|
|
ButtonSegment<ThemeMode>(
|
|
value: ThemeMode.dark,
|
|
label: Text(S.of(context)!.dark),
|
|
icon: const Icon(Icons.dark_mode_rounded),
|
|
),
|
|
ButtonSegment<ThemeMode>(
|
|
value: ThemeMode.system,
|
|
label: Text(S.of(context)!.system),
|
|
icon: const Icon(Icons.auto_mode_rounded),
|
|
),
|
|
],
|
|
selected: {themeProvider.themeMode},
|
|
onSelectionChanged: (Set<ThemeMode> newSelection) {
|
|
themeProvider.setThemeMode(newSelection.first);
|
|
},
|
|
style: ButtonStyle(
|
|
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return colorScheme.primaryContainer;
|
|
}
|
|
return colorScheme.surfaceVariant.withOpacity(0.5);
|
|
}),
|
|
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.selected)) {
|
|
return colorScheme.onPrimaryContainer;
|
|
}
|
|
return colorScheme.onSurfaceVariant;
|
|
}),
|
|
side: WidgetStateProperty.all(BorderSide.none),
|
|
shape: WidgetStateProperty.all(
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Accent color picker
|
|
Consumer<ThemeProvider>(
|
|
builder: (context, themeProvider, _) {
|
|
final isDisabled = themeProvider.useMaterialTheme;
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
S.of(context)!.accentColor,
|
|
style: TextStyle(
|
|
color: isDisabled
|
|
? colorScheme.onSurface.withOpacity(0.4)
|
|
: colorScheme.onSurface,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: accentColorOptions.map((color) {
|
|
final isSelected = themeProvider.customColor.value == color.value;
|
|
return GestureDetector(
|
|
onTap: isDisabled ? null : () {
|
|
themeProvider.setCustomColor(color);
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
curve: Curves.easeInOut,
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: isDisabled ? color.withOpacity(0.3) : color,
|
|
shape: BoxShape.circle,
|
|
border: isSelected && !isDisabled
|
|
? Border.all(color: colorScheme.onSurface, width: 3)
|
|
: null,
|
|
boxShadow: isSelected && !isDisabled
|
|
? [BoxShadow(color: color.withOpacity(0.5), blurRadius: 8, spreadRadius: 2)]
|
|
: null,
|
|
),
|
|
child: isSelected && !isDisabled
|
|
? Icon(Icons.check, color: Colors.white, size: 20)
|
|
: null,
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
Consumer<ThemeProvider>(
|
|
builder: (context, themeProvider, _) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.materialYou,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.materialYouDescription,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: themeProvider.useMaterialTheme,
|
|
onChanged: (value) {
|
|
themeProvider.setUseMaterialTheme(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
Consumer<ThemeProvider>(
|
|
builder: (context, themeProvider, _) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.adaptiveTheme,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.extractColorsFromArtwork,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: themeProvider.adaptiveTheme,
|
|
onChanged: (value) {
|
|
themeProvider.setAdaptiveTheme(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Language section
|
|
Text(
|
|
S.of(context)!.language,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Consumer<LocaleProvider>(
|
|
builder: (context, localeProvider, _) {
|
|
String getLanguageName(String? code) {
|
|
switch (code) {
|
|
case 'en':
|
|
return 'English';
|
|
case 'de':
|
|
return 'Deutsch';
|
|
case 'es':
|
|
return 'Español';
|
|
default:
|
|
return S.of(context)!.system;
|
|
}
|
|
}
|
|
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
tileColor: colorScheme.surfaceVariant.withOpacity(0.5),
|
|
leading: Icon(Icons.language_rounded, color: colorScheme.onSurfaceVariant),
|
|
title: Text(S.of(context)!.language),
|
|
subtitle: Text(
|
|
getLanguageName(localeProvider.locale?.languageCode),
|
|
style: TextStyle(color: colorScheme.primary),
|
|
),
|
|
trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant),
|
|
onTap: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return AlertDialog(
|
|
title: Text(S.of(context)!.language),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
RadioListTile<String?>(
|
|
title: Text(S.of(context)!.system),
|
|
subtitle: Text(S.of(context)!.auto),
|
|
value: null,
|
|
groupValue: localeProvider.locale?.languageCode,
|
|
onChanged: (value) {
|
|
localeProvider.setLocale(null);
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
),
|
|
RadioListTile<String?>(
|
|
title: Text(S.of(context)!.english),
|
|
value: 'en',
|
|
groupValue: localeProvider.locale?.languageCode,
|
|
onChanged: (value) {
|
|
localeProvider.setLocale(const Locale('en'));
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
),
|
|
RadioListTile<String?>(
|
|
title: Text(S.of(context)!.german),
|
|
value: 'de',
|
|
groupValue: localeProvider.locale?.languageCode,
|
|
onChanged: (value) {
|
|
localeProvider.setLocale(const Locale('de'));
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
),
|
|
RadioListTile<String?>(
|
|
title: Text(S.of(context)!.spanish),
|
|
value: 'es',
|
|
groupValue: localeProvider.locale?.languageCode,
|
|
onChanged: (value) {
|
|
localeProvider.setLocale(const Locale('es'));
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
),
|
|
RadioListTile<String?>(
|
|
title: Text(S.of(context)!.french),
|
|
value: 'fr',
|
|
groupValue: localeProvider.locale?.languageCode,
|
|
onChanged: (value) {
|
|
localeProvider.setLocale(const Locale('fr'));
|
|
Navigator.pop(dialogContext);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Player section
|
|
Text(
|
|
S.of(context)!.players,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.preferLocalPlayer,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.preferLocalPlayerDescription,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: _preferLocalPlayer,
|
|
onChanged: (value) {
|
|
setState(() => _preferLocalPlayer = value);
|
|
SettingsService.setPreferLocalPlayer(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.smartSortPlayers,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.smartSortPlayersDescription,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: _smartSortPlayers,
|
|
onChanged: (value) {
|
|
setState(() => _smartSortPlayers = value);
|
|
SettingsService.setSmartSortPlayers(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.disableAutoSwitch,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.disableAutoSwitchDescription,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: _disableAutoSwitch,
|
|
onChanged: (value) {
|
|
setState(() => _disableAutoSwitch = value);
|
|
SettingsService.setDisableAutoSwitch(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
'Volume precision mode',
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
'Hold still while adjusting volume for fine control',
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: _volumePrecisionMode,
|
|
onChanged: (value) {
|
|
setState(() => _volumePrecisionMode = value);
|
|
SettingsService.setVolumePrecisionMode(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Hints & Tips section
|
|
Text(
|
|
S.of(context)!.hintsAndTips,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
S.of(context)!.showHintsDescription,
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onBackground.withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.showHints,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
value: _showHints,
|
|
onChanged: (value) {
|
|
setState(() => _showHints = value);
|
|
SettingsService.setShowHints(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Display section
|
|
Text(
|
|
S.of(context)!.display,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
S.of(context)!.showProviderIcons,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
S.of(context)!.showProviderIconsDescription,
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurface.withOpacity(0.6),
|
|
),
|
|
),
|
|
value: _showProviderIcons,
|
|
onChanged: (value) {
|
|
setState(() => _showProviderIcons = value);
|
|
SettingsService.setShowProviderIcons(value);
|
|
},
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Home Screen section
|
|
Text(
|
|
S.of(context)!.homeScreen,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
S.of(context)!.chooseHomeScreenRows,
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onBackground.withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Reorderable home rows list
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: ReorderableListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
buildDefaultDragHandles: false,
|
|
itemCount: _homeRowOrder.length,
|
|
onReorder: (oldIndex, newIndex) {
|
|
setState(() {
|
|
if (newIndex > oldIndex) newIndex--;
|
|
final item = _homeRowOrder.removeAt(oldIndex);
|
|
_homeRowOrder.insert(newIndex, item);
|
|
});
|
|
SettingsService.setHomeRowOrder(_homeRowOrder);
|
|
},
|
|
itemBuilder: (context, index) {
|
|
final rowId = _homeRowOrder[index];
|
|
final rowInfo = _getRowInfo(rowId);
|
|
final isEnabled = _getRowEnabled(rowId);
|
|
|
|
return Container(
|
|
key: ValueKey(rowId),
|
|
decoration: BoxDecoration(
|
|
border: index < _homeRowOrder.length - 1
|
|
? Border(bottom: BorderSide(color: colorScheme.outline.withOpacity(0.2)))
|
|
: null,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
ReorderableDragStartListener(
|
|
index: index,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
|
child: Icon(
|
|
Icons.drag_handle,
|
|
color: colorScheme.onSurface.withOpacity(0.5),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: SwitchListTile(
|
|
title: Text(
|
|
rowInfo['title']!,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
),
|
|
subtitle: Text(
|
|
rowInfo['subtitle']!,
|
|
style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 12),
|
|
),
|
|
value: isEnabled,
|
|
onChanged: (value) => _setRowEnabled(rowId, value),
|
|
activeColor: colorScheme.primary,
|
|
contentPadding: const EdgeInsets.only(right: 8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
Text(
|
|
S.of(context)!.metadataApis,
|
|
style: textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
S.of(context)!.metadataApisDescription,
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onBackground.withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
TextField(
|
|
controller: _lastFmApiKeyController,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
decoration: InputDecoration(
|
|
labelText: S.of(context)!.lastFmApiKey,
|
|
hintText: S.of(context)!.lastFmApiKeyHint,
|
|
hintStyle: TextStyle(color: colorScheme.onSurface.withOpacity(0.38)),
|
|
filled: true,
|
|
fillColor: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
prefixIcon: Icon(
|
|
Icons.music_note_rounded,
|
|
color: colorScheme.onSurface.withOpacity(0.54),
|
|
),
|
|
suffixIcon: _lastFmApiKeyController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
setState(() {
|
|
_lastFmApiKeyController.clear();
|
|
});
|
|
SettingsService.setLastFmApiKey(null);
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (value) {
|
|
SettingsService.setLastFmApiKey(value.trim().isEmpty ? null : value.trim());
|
|
setState(() {}); // Update UI to show/hide clear button
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
TextField(
|
|
controller: _audioDbApiKeyController,
|
|
style: TextStyle(color: colorScheme.onSurface),
|
|
decoration: InputDecoration(
|
|
labelText: S.of(context)!.theAudioDbApiKey,
|
|
hintText: S.of(context)!.theAudioDbApiKeyHint,
|
|
hintStyle: TextStyle(color: colorScheme.onSurface.withOpacity(0.38)),
|
|
filled: true,
|
|
fillColor: colorScheme.surfaceVariant.withOpacity(0.3),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
prefixIcon: Icon(
|
|
Icons.audiotrack_rounded,
|
|
color: colorScheme.onSurface.withOpacity(0.54),
|
|
),
|
|
suffixIcon: _audioDbApiKeyController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
setState(() {
|
|
_audioDbApiKeyController.clear();
|
|
});
|
|
SettingsService.setTheAudioDbApiKey(null);
|
|
},
|
|
)
|
|
: null,
|
|
),
|
|
onChanged: (value) {
|
|
SettingsService.setTheAudioDbApiKey(value.trim().isEmpty ? null : value.trim());
|
|
setState(() {}); // Update UI to show/hide clear button
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: FilledButton.tonalIcon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const DebugLogScreen(),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.bug_report_rounded),
|
|
label: Text(S.of(context)!.viewDebugLogs),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: colorScheme.surfaceVariant.withOpacity(0.5),
|
|
foregroundColor: colorScheme.onSurfaceVariant,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: BottomSpacing.navBarOnly), // Space for bottom nav bar
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
IconData _getStatusIcon(MAConnectionState state) {
|
|
switch (state) {
|
|
case MAConnectionState.connected:
|
|
case MAConnectionState.authenticated:
|
|
return Icons.check_circle_rounded;
|
|
case MAConnectionState.connecting:
|
|
case MAConnectionState.authenticating:
|
|
return Icons.sync_rounded;
|
|
case MAConnectionState.error:
|
|
return Icons.error_rounded;
|
|
case MAConnectionState.disconnected:
|
|
return Icons.cloud_off_rounded;
|
|
}
|
|
}
|
|
|
|
Color _getStatusColor(MAConnectionState state, ColorScheme colorScheme) {
|
|
switch (state) {
|
|
case MAConnectionState.connected:
|
|
case MAConnectionState.authenticated:
|
|
return Colors.green;
|
|
case MAConnectionState.connecting:
|
|
case MAConnectionState.authenticating:
|
|
return Colors.orange;
|
|
case MAConnectionState.error:
|
|
return colorScheme.error;
|
|
case MAConnectionState.disconnected:
|
|
return colorScheme.onSurface.withOpacity(0.5);
|
|
}
|
|
}
|
|
|
|
String _getStatusText(MAConnectionState state) {
|
|
final s = S.of(context)!;
|
|
switch (state) {
|
|
case MAConnectionState.connected:
|
|
case MAConnectionState.authenticated:
|
|
return s.connected;
|
|
case MAConnectionState.connecting:
|
|
case MAConnectionState.authenticating:
|
|
return s.connecting;
|
|
case MAConnectionState.error:
|
|
return s.connectionError;
|
|
case MAConnectionState.disconnected:
|
|
return s.disconnected;
|
|
}
|
|
}
|
|
|
|
}
|