From d07a49f6050588a03638bf1cda6d7d6c3fd89713 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 11 Jan 2026 06:41:34 +0700 Subject: [PATCH] UI modernization: Global theme update, redesigned Options/Download settings, and smart filename editor --- CHANGELOG.md | 43 +- .../settings/download_settings_page.dart | 587 ++++++++--- .../settings/options_settings_page.dart | 965 ++++++++++++------ lib/theme/app_theme.dart | 189 ++-- 4 files changed, 1227 insertions(+), 557 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d815009..1e13a7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,44 @@ - Fallback to text search (artist + track name) when ISRC not found in Deezer - Progress dialog shows enrichment status during import - Ensures downloaded files have proper cover art and metadata +- **Deezer Metadata Support**: Enhanced metadata viewer for Deezer tracks + - "Open in Deezer" button for Deezer-sourced tracks (opens app or web) + - Displays "Deezer ID" instead of "Spotify ID" when applicable +- **Smart Tag Injection**: Filename format editor intelligently handles separators + - Auto-detects if " - " is needed between tags + - Prevents double separators or missing spaces +- **Dynamic Source Info**: Search source selector now shows helpful context + - "No login required" for Deezer + - "Requires credentials" for Spotify + +### Changed + +- **UI Modernization**: Major UI consistency updates across the app + - **Unified App Bars**: Home, History, and Settings now share identical behavior + - Lowered expanded header for easier one-handed reachability + - Dynamic title text scaling (20px to 34px) + - **Appearance Settings**: Completely redesigned appearance page + - New "Theme Preview" card showing visualizing current theme + - Modern color palette picker replacing old color dots + - Clean, grouped layout + - "AMOLED Dark" switch is now hidden when using Light Mode + - **App Logo**: Refined logo style on Home and About screens + - Inverted colors: Filled primary color circle with on-color icon + - Removed padding for a cleaner, bolder look + - **Material 3 Switches**: Added checkmark icon to active switches +- **UI Modernization (Global)**: Complete design refresh for a cleaner, modern look + - **Rounded Corners**: Standardized 16px radius for all cards, buttons, and input fields + - **Transparent Elements**: Applied subtle transparency to input fields and containers using `surfaceContainerHighest` + - **Consistent Buttons**: Unified button styling across the app (pill shape, 16px radius) +- **Options Settings Redesign**: improved layout and usability + - **Search Source Priority**: Moved "Search Source" section to the very top for quick access + - **Compact Source Selector**: Redesigned provider toggle (Deezer/Spotify) to be compact and consistent + - **Credentials Workflow**: Reorganized Custom Credentials settings; toggle now auto-prompts if credentials missing + - **Modern Credentials Dialog**: Totally redesigned input dialog for Spotify Client ID/Secret +- **Filename Format Editor 2.0**: + - **Modern Sheet UI**: Replaced legacy dialog with a clean, full-width bottom sheet + - **Tag Chips**: Added clickable chips ({artist}, {title}) for one-tap insertion + - **Smart Formatting**: Automatically injects separators (" - ") when adding tags for faster editing ### Fixed @@ -18,7 +56,7 @@ - **CSV Import Missing Duration**: Fixed duration showing 0:00 for CSV-imported tracks - Duration now fetched from Deezer metadata during enrichment - **Disc Number Not Displayed**: Fixed disc number not showing in track metadata screen - - Changed condition from `discNumber > 1` to `discNumber > 0` + - Changed condition from `discNumber > 0` to `discNumber > 0` - Now displays disc 1 instead of hiding it - **Download History Using Wrong Track Data**: Fixed history using original CSV data instead of enriched data - Now uses `trackToDownload` (enriched) instead of `item.track` (original) @@ -38,6 +76,9 @@ - Added `TotalSamples` to `AudioQuality` struct for duration calculation - Updated `go_backend/exports.go`: - `ReadFileMetadata` now returns duration calculated from FLAC stream info +- Updated `AppTheme` with new `InputDecorationTheme` and `ButtonTheme` definitions +- Refactored `DownloadSettingsPage` to use new `_showFormatEditor` with cursor-aware capabilities +- Optimized various dialogs to use `showModalBottomSheet` with `isScrollControlled` for better keyboard handling --- diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 05848ddb..c08fdadd 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -28,16 +28,25 @@ class DownloadSettingsPage extends ConsumerWidget { pinned: true, backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), flexibleSpace: LayoutBuilder( builder: (context, constraints) { final maxHeight = 120 + topPadding; final minHeight = kToolbarHeight + topPadding; - final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); final leftPadding = 56 - (32 * expandRatio); // 56 -> 24 return FlexibleSpaceBar( expandedTitleScale: 1.0, - titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), title: Text( 'Download', style: TextStyle( @@ -51,89 +60,117 @@ class DownloadSettingsPage extends ConsumerWidget { ), ), - // Service section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Service')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - _ServiceSelector( - currentService: settings.defaultService, - onChanged: (service) => ref.read(settingsProvider.notifier).setDefaultService(service), - ), - ], + // Service section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Service'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _ServiceSelector( + currentService: settings.defaultService, + onChanged: (service) => ref + .read(settingsProvider.notifier) + .setDefaultService(service), + ), + ], + ), ), - ), - // Quality section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Audio Quality')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.tune, - title: 'Ask Before Download', - subtitle: 'Choose quality for each download', - value: settings.askQualityBeforeDownload, - onChanged: (value) => ref.read(settingsProvider.notifier).setAskQualityBeforeDownload(value), - ), - if (!settings.askQualityBeforeDownload) ...[ - _QualityOption( - title: 'FLAC Lossless', - subtitle: '16-bit / 44.1kHz', - isSelected: settings.audioQuality == 'LOSSLESS', - onTap: () => ref.read(settingsProvider.notifier).setAudioQuality('LOSSLESS'), + // Quality section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Audio Quality'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.tune, + title: 'Ask Before Download', + subtitle: 'Choose quality for each download', + value: settings.askQualityBeforeDownload, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setAskQualityBeforeDownload(value), ), - _QualityOption( - title: 'Hi-Res FLAC', - subtitle: '24-bit / up to 96kHz', - isSelected: settings.audioQuality == 'HI_RES', - onTap: () => ref.read(settingsProvider.notifier).setAudioQuality('HI_RES'), + if (!settings.askQualityBeforeDownload) ...[ + _QualityOption( + title: 'FLAC Lossless', + subtitle: '16-bit / 44.1kHz', + isSelected: settings.audioQuality == 'LOSSLESS', + onTap: () => ref + .read(settingsProvider.notifier) + .setAudioQuality('LOSSLESS'), + ), + _QualityOption( + title: 'Hi-Res FLAC', + subtitle: '24-bit / up to 96kHz', + isSelected: settings.audioQuality == 'HI_RES', + onTap: () => ref + .read(settingsProvider.notifier) + .setAudioQuality('HI_RES'), + ), + _QualityOption( + title: 'Hi-Res FLAC Max', + subtitle: '24-bit / up to 192kHz', + isSelected: settings.audioQuality == 'HI_RES_LOSSLESS', + onTap: () => ref + .read(settingsProvider.notifier) + .setAudioQuality('HI_RES_LOSSLESS'), + showDivider: false, + ), + ], + ], + ), + ), + + // File settings section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'File Settings'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.text_fields, + title: 'Filename Format', + subtitle: settings.filenameFormat, + onTap: () => _showFormatEditor( + context, + ref, + settings.filenameFormat, + ), ), - _QualityOption( - title: 'Hi-Res FLAC Max', - subtitle: '24-bit / up to 192kHz', - isSelected: settings.audioQuality == 'HI_RES_LOSSLESS', - onTap: () => ref.read(settingsProvider.notifier).setAudioQuality('HI_RES_LOSSLESS'), + SettingsItem( + icon: Icons.folder_outlined, + title: 'Download Directory', + subtitle: settings.downloadDirectory.isEmpty + ? (Platform.isIOS + ? 'App Documents Folder' + : 'Music/SpotiFLAC') + : settings.downloadDirectory, + onTap: () => _pickDirectory(context, ref), + ), + SettingsItem( + icon: Icons.create_new_folder_outlined, + title: 'Folder Organization', + subtitle: _getFolderOrganizationLabel( + settings.folderOrganization, + ), + onTap: () => _showFolderOrganizationPicker( + context, + ref, + settings.folderOrganization, + ), showDivider: false, ), ], - ], + ), ), - ), - // File settings section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'File Settings')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.text_fields, - title: 'Filename Format', - subtitle: settings.filenameFormat, - onTap: () => _showFormatEditor(context, ref, settings.filenameFormat), - ), - SettingsItem( - icon: Icons.folder_outlined, - title: 'Download Directory', - subtitle: settings.downloadDirectory.isEmpty - ? (Platform.isIOS ? 'App Documents Folder' : 'Music/SpotiFLAC') - : settings.downloadDirectory, - onTap: () => _pickDirectory(context, ref), - ), - SettingsItem( - icon: Icons.create_new_folder_outlined, - title: 'Folder Organization', - subtitle: _getFolderOrganizationLabel(settings.folderOrganization), - onTap: () => _showFolderOrganizationPicker(context, ref, settings.folderOrganization), - showDivider: false, - ), - ], - ), - ), - - const SliverToBoxAdapter(child: SizedBox(height: 32)), - ], - ), + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), ), ); } @@ -141,26 +178,176 @@ class DownloadSettingsPage extends ConsumerWidget { void _showFormatEditor(BuildContext context, WidgetRef ref, String current) { final controller = TextEditingController(text: current); final colorScheme = Theme.of(context).colorScheme; + + final tags = [ + '{artist}', + '{title}', + '{album}', + '{track}', + '{year}', + '{disc}', + ]; + + void insertTag(String tag) { + final text = controller.text; + final selection = controller.selection; + final start = selection.start >= 0 ? selection.start : text.length; + final end = selection.end >= 0 ? selection.end : text.length; + + String insertion = tag; + if (start > 0) { + final before = text.substring(0, start); + // Smart separator: if not starting a file and no hyphen separator exists, add " - " + if (!before.trim().endsWith('-')) { + insertion = ' - $tag'; + } else if (before.trim().endsWith('-') && !before.endsWith(' ')) { + // If ends with '-' but no space, add space + insertion = ' $tag'; + } + } + + final newText = text.replaceRange(start, end, insertion); + controller.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed(offset: start + insertion.length), + ); + } + showModalBottomSheet( - context: context, isScrollControlled: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + context: context, + isScrollControlled: true, + backgroundColor: colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), builder: (context) => Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, MediaQuery.of(context).viewInsets.bottom + 24), - child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Filename Format', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - TextField(controller: controller, decoration: const InputDecoration(hintText: '{artist} - {title}'), autofocus: true), - const SizedBox(height: 16), - Text('Available: {title}, {artist}, {album}, {track}, {year}, {disc}', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant)), - const SizedBox(height: 24), - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), - const SizedBox(width: 8), - FilledButton(onPressed: () { ref.read(settingsProvider.notifier).setFilenameFormat(controller.text); Navigator.pop(context); }, child: const Text('Save')), - ]), - ]), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Text( + 'Filename Format', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Customize how your files are named.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + TextField( + controller: controller, + decoration: InputDecoration( + hintText: '{artist} - {title}', + filled: true, + fillColor: colorScheme.surfaceContainerHighest.withValues( + alpha: 0.3, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + ), + autofocus: true, + ), + const SizedBox(height: 24), + + Text( + 'Tap to insert tag:', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: tags.map((tag) { + return ActionChip( + label: Text(tag), + onPressed: () => insertTag(tag), + backgroundColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + labelStyle: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ); + }).toList(), + ), + + const SizedBox(height: 32), + + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton( + onPressed: () { + ref + .read(settingsProvider.notifier) + .setFilenameFormat(controller.text); + Navigator.pop(context); + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text('Save Format'), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ), ), ); } @@ -172,7 +359,8 @@ class DownloadSettingsPage extends ConsumerWidget { } else { // Android: Use file picker final result = await FilePicker.platform.getDirectoryPath(); - if (result != null) ref.read(settingsProvider.notifier).setDownloadDirectory(result); + if (result != null) + ref.read(settingsProvider.notifier).setDownloadDirectory(result); } } @@ -181,7 +369,9 @@ class DownloadSettingsPage extends ConsumerWidget { showModalBottomSheet( context: context, backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, @@ -189,13 +379,20 @@ class DownloadSettingsPage extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + child: Text( + 'Download Location', + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( 'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), ), ), ListTile( @@ -205,7 +402,9 @@ class DownloadSettingsPage extends ConsumerWidget { trailing: Icon(Icons.check_circle, color: colorScheme.primary), onTap: () async { final dir = await getApplicationDocumentsDirectory(); - ref.read(settingsProvider.notifier).setDownloadDirectory(dir.path); + ref + .read(settingsProvider.notifier) + .setDownloadDirectory(dir.path); if (ctx.mounted) Navigator.pop(ctx); }, ), @@ -218,7 +417,9 @@ class DownloadSettingsPage extends ConsumerWidget { // Note: iOS requires folder to have at least one file to be selectable final result = await FilePicker.platform.getDirectoryPath(); if (result != null) { - ref.read(settingsProvider.notifier).setDownloadDirectory(result); + ref + .read(settingsProvider.notifier) + .setDownloadDirectory(result); } }, ), @@ -232,12 +433,18 @@ class DownloadSettingsPage extends ConsumerWidget { ), child: Row( children: [ - Icon(Icons.info_outline, size: 20, color: colorScheme.tertiary), + Icon( + Icons.info_outline, + size: 20, + color: colorScheme.tertiary, + ), const SizedBox(width: 12), Expanded( child: Text( 'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onTertiaryContainer, + ), ), ), ], @@ -264,12 +471,18 @@ class DownloadSettingsPage extends ConsumerWidget { } } - void _showFolderOrganizationPicker(BuildContext context, WidgetRef ref, String current) { + void _showFolderOrganizationPicker( + BuildContext context, + WidgetRef ref, + String current, + ) { final colorScheme = Theme.of(context).colorScheme; showModalBottomSheet( context: context, backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, @@ -277,39 +490,69 @@ class DownloadSettingsPage extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text('Folder Organization', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), + child: Text( + 'Folder Organization', + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text('Organize downloaded files into folders', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + child: Text( + 'Organize downloaded files into folders', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), ), _FolderOption( title: 'None', subtitle: 'All files in download folder', example: 'SpotiFLAC/Track.flac', isSelected: current == 'none', - onTap: () { ref.read(settingsProvider.notifier).setFolderOrganization('none'); Navigator.pop(context); }, + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('none'); + Navigator.pop(context); + }, ), _FolderOption( title: 'By Artist', subtitle: 'Separate folder for each artist', example: 'SpotiFLAC/Artist Name/Track.flac', isSelected: current == 'artist', - onTap: () { ref.read(settingsProvider.notifier).setFolderOrganization('artist'); Navigator.pop(context); }, + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('artist'); + Navigator.pop(context); + }, ), _FolderOption( title: 'By Album', subtitle: 'Separate folder for each album', example: 'SpotiFLAC/Album Name/Track.flac', isSelected: current == 'album', - onTap: () { ref.read(settingsProvider.notifier).setFolderOrganization('album'); Navigator.pop(context); }, + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('album'); + Navigator.pop(context); + }, ), _FolderOption( title: 'By Artist & Album', subtitle: 'Nested folders for artist and album', example: 'SpotiFLAC/Artist/Album/Track.flac', isSelected: current == 'artist_album', - onTap: () { ref.read(settingsProvider.notifier).setFolderOrganization('artist_album'); Navigator.pop(context); }, + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization('artist_album'); + Navigator.pop(context); + }, ), const SizedBox(height: 16), ], @@ -322,19 +565,39 @@ class DownloadSettingsPage extends ConsumerWidget { class _ServiceSelector extends StatelessWidget { final String currentService; final ValueChanged onChanged; - const _ServiceSelector({required this.currentService, required this.onChanged}); + const _ServiceSelector({ + required this.currentService, + required this.onChanged, + }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(12), - child: Row(children: [ - _ServiceChip(icon: Icons.music_note, label: 'Tidal', isSelected: currentService == 'tidal', onTap: () => onChanged('tidal')), - const SizedBox(width: 8), - _ServiceChip(icon: Icons.album, label: 'Qobuz', isSelected: currentService == 'qobuz', onTap: () => onChanged('qobuz')), - const SizedBox(width: 8), - _ServiceChip(icon: Icons.shopping_bag, label: 'Amazon', isSelected: currentService == 'amazon', onTap: () => onChanged('amazon')), - ]), + child: Row( + children: [ + _ServiceChip( + icon: Icons.music_note, + label: 'Tidal', + isSelected: currentService == 'tidal', + onTap: () => onChanged('tidal'), + ), + const SizedBox(width: 8), + _ServiceChip( + icon: Icons.album, + label: 'Qobuz', + isSelected: currentService == 'qobuz', + onTap: () => onChanged('qobuz'), + ), + const SizedBox(width: 8), + _ServiceChip( + icon: Icons.shopping_bag, + label: 'Amazon', + isSelected: currentService == 'amazon', + onTap: () => onChanged('amazon'), + ), + ], + ), ); } } @@ -344,17 +607,25 @@ class _ServiceChip extends StatelessWidget { final String label; final bool isSelected; final VoidCallback onTap; - const _ServiceChip({required this.icon, required this.label, required this.isSelected, required this.onTap}); + const _ServiceChip({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface) + + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) : colorScheme.surfaceContainerHigh; - + return Expanded( child: Material( color: isSelected ? colorScheme.primaryContainer : unselectedColor, @@ -364,13 +635,29 @@ class _ServiceChip extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), - child: Column(children: [ - Icon(icon, color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant), - const SizedBox(height: 6), - Text(label, style: TextStyle(fontSize: 12, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant)), - ]), + child: Column( + children: [ + Icon( + icon, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ), ), @@ -384,7 +671,13 @@ class _QualityOption extends StatelessWidget { final bool isSelected; final VoidCallback onTap; final bool showDivider; - const _QualityOption({required this.title, required this.subtitle, required this.isSelected, required this.onTap, this.showDivider = true}); + const _QualityOption({ + required this.title, + required this.subtitle, + required this.isSelected, + required this.onTap, + this.showDivider = true, + }); @override Widget build(BuildContext context) { @@ -404,11 +697,16 @@ class _QualityOption extends StatelessWidget { children: [ Text(title, style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 2), - Text(subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), ], ), ), - isSelected + isSelected ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), ], @@ -434,7 +732,13 @@ class _FolderOption extends StatelessWidget { final String example; final bool isSelected; final VoidCallback onTap; - const _FolderOption({required this.title, required this.subtitle, required this.example, required this.isSelected, required this.onTap}); + const _FolderOption({ + required this.title, + required this.subtitle, + required this.example, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { @@ -447,10 +751,19 @@ class _FolderOption extends StatelessWidget { children: [ Text(subtitle), const SizedBox(height: 4), - Text(example, style: TextStyle(fontFamily: 'monospace', fontSize: 11, color: colorScheme.primary)), + Text( + example, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: colorScheme.primary, + ), + ), ], ), - trailing: isSelected ? Icon(Icons.check_circle, color: colorScheme.primary) : Icon(Icons.circle_outlined, color: colorScheme.outline), + trailing: isSelected + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), onTap: onTap, ); } diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index c34f06a7..f9822fb6 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -27,16 +27,25 @@ class OptionsSettingsPage extends ConsumerWidget { pinned: true, backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context)), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), flexibleSpace: LayoutBuilder( builder: (context, constraints) { final maxHeight = 120 + topPadding; final minHeight = kToolbarHeight + topPadding; - final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); final leftPadding = 56 - (32 * expandRatio); // 56 -> 24 return FlexibleSpaceBar( expandedTitleScale: 1.0, - titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), title: Text( 'Options', style: TextStyle( @@ -50,156 +59,200 @@ class OptionsSettingsPage extends ConsumerWidget { ), ), - // Download options section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Download')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.sync, - title: 'Auto Fallback', - subtitle: 'Try other services if download fails', - value: settings.autoFallback, - onChanged: (v) => ref.read(settingsProvider.notifier).setAutoFallback(v), - ), - SettingsSwitchItem( - icon: Icons.lyrics, - title: 'Embed Lyrics', - subtitle: 'Embed synced lyrics into FLAC files', - value: settings.embedLyrics, - onChanged: (v) => ref.read(settingsProvider.notifier).setEmbedLyrics(v), - ), - SettingsSwitchItem( - icon: Icons.image, - title: 'Max Quality Cover', - subtitle: 'Download highest resolution cover art', - value: settings.maxQualityCover, - onChanged: (v) => ref.read(settingsProvider.notifier).setMaxQualityCover(v), - showDivider: false, - ), - ], + // Search Source section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Search Source'), ), - ), - - // Performance section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Performance')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - _ConcurrentDownloadsItem( - currentValue: settings.concurrentDownloads, - onChanged: (v) => ref.read(settingsProvider.notifier).setConcurrentDownloads(v), - ), - ], - ), - ), - - // App section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'App')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.system_update, - title: 'Check for Updates', - subtitle: 'Notify when new version is available', - value: settings.checkForUpdates, - onChanged: (v) => ref.read(settingsProvider.notifier).setCheckForUpdates(v), - ), - _UpdateChannelSelector( - currentChannel: settings.updateChannel, - onChanged: (v) => ref.read(settingsProvider.notifier).setUpdateChannel(v), - ), - ], - ), - ), - - // Spotify API section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Spotify API')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - _MetadataSourceSelector( - currentSource: settings.metadataSource, - onChanged: (v) => ref.read(settingsProvider.notifier).setMetadataSource(v), - ), - if (settings.metadataSource == 'spotify') ...[ - SettingsItem( - icon: Icons.key, - title: 'Custom Credentials', - subtitle: settings.spotifyClientId.isNotEmpty - ? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}' - : 'Not configured', - onTap: () => _showSpotifyCredentialsDialog(context, ref, settings), - trailing: settings.spotifyClientId.isNotEmpty - ? Icon(Icons.edit, color: Theme.of(context).colorScheme.onSurfaceVariant, size: 20) - : Icon(Icons.add, color: Theme.of(context).colorScheme.primary, size: 20), - showDivider: settings.spotifyClientId.isNotEmpty, + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _MetadataSourceSelector( + currentSource: settings.metadataSource, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setMetadataSource(v), ), - if (settings.spotifyClientId.isNotEmpty) + if (settings.metadataSource == 'spotify') ...[ SettingsSwitchItem( icon: Icons.toggle_on, title: 'Use Custom Credentials', - subtitle: settings.useCustomSpotifyCredentials - ? 'Using your credentials' + subtitle: settings.useCustomSpotifyCredentials + ? 'Using your credentials' : 'Using default credentials', value: settings.useCustomSpotifyCredentials, - onChanged: (v) => ref.read(settingsProvider.notifier).setUseCustomSpotifyCredentials(v), + onChanged: (v) { + ref + .read(settingsProvider.notifier) + .setUseCustomSpotifyCredentials(v); + if (v && settings.spotifyClientId.isEmpty) { + _showSpotifyCredentialsDialog(context, ref, settings); + } + }, + showDivider: true, + ), + SettingsItem( + icon: Icons.key, + title: 'Set Credentials', + subtitle: settings.spotifyClientId.isNotEmpty + ? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}' + : 'Not configured', + onTap: () => + _showSpotifyCredentialsDialog(context, ref, settings), + trailing: Icon( + settings.spotifyClientId.isNotEmpty + ? Icons.edit + : Icons.add, + color: settings.spotifyClientId.isNotEmpty + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.primary, + size: 20, + ), showDivider: false, ), + ], ], - ], + ), ), - ), - // Data section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Data')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.delete_forever, - title: 'Clear Download History', - subtitle: 'Remove all downloaded tracks from history', - onTap: () => _showClearHistoryDialog(context, ref, colorScheme), - showDivider: false, - ), - ], + // Download options section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Download'), ), - ), - - // Debug section - const SliverToBoxAdapter(child: SettingsSectionHeader(title: 'Debug')), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.bug_report, - title: 'Detailed Logging', - subtitle: settings.enableLogging - ? 'Detailed logs are being recorded' - : 'Enable for bug reports', - value: settings.enableLogging, - onChanged: (v) => ref.read(settingsProvider.notifier).setEnableLogging(v), - showDivider: false, - ), - ], + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.sync, + title: 'Auto Fallback', + subtitle: 'Try other services if download fails', + value: settings.autoFallback, + onChanged: (v) => + ref.read(settingsProvider.notifier).setAutoFallback(v), + ), + SettingsSwitchItem( + icon: Icons.lyrics, + title: 'Embed Lyrics', + subtitle: 'Embed synced lyrics into FLAC files', + value: settings.embedLyrics, + onChanged: (v) => + ref.read(settingsProvider.notifier).setEmbedLyrics(v), + ), + SettingsSwitchItem( + icon: Icons.image, + title: 'Max Quality Cover', + subtitle: 'Download highest resolution cover art', + value: settings.maxQualityCover, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setMaxQualityCover(v), + showDivider: false, + ), + ], + ), ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 32)), - ], - ), + // Performance section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Performance'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + _ConcurrentDownloadsItem( + currentValue: settings.concurrentDownloads, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setConcurrentDownloads(v), + ), + ], + ), + ), + + // App section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'App'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.system_update, + title: 'Check for Updates', + subtitle: 'Notify when new version is available', + value: settings.checkForUpdates, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setCheckForUpdates(v), + ), + _UpdateChannelSelector( + currentChannel: settings.updateChannel, + onChanged: (v) => + ref.read(settingsProvider.notifier).setUpdateChannel(v), + ), + ], + ), + ), + + // Data section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Data'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.delete_forever, + title: 'Clear Download History', + subtitle: 'Remove all downloaded tracks from history', + onTap: () => + _showClearHistoryDialog(context, ref, colorScheme), + showDivider: false, + ), + ], + ), + ), + + // Debug section + const SliverToBoxAdapter( + child: SettingsSectionHeader(title: 'Debug'), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.bug_report, + title: 'Detailed Logging', + subtitle: settings.enableLogging + ? 'Detailed logs are being recorded' + : 'Enable for bug reports', + value: settings.enableLogging, + onChanged: (v) => + ref.read(settingsProvider.notifier).setEnableLogging(v), + showDivider: false, + ), + ], + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), ), ); } - void _showClearHistoryDialog(BuildContext context, WidgetRef ref, ColorScheme colorScheme) { + void _showClearHistoryDialog( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Clear History'), - content: const Text('Are you sure you want to clear all download history? This cannot be undone.'), + content: const Text( + 'Are you sure you want to clear all download history? This cannot be undone.', + ), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -209,9 +262,9 @@ class OptionsSettingsPage extends ConsumerWidget { onPressed: () { ref.read(downloadHistoryProvider.notifier).clearHistory(); Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('History cleared')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('History cleared'))); }, child: Text('Clear', style: TextStyle(color: colorScheme.error)), ), @@ -220,125 +273,192 @@ class OptionsSettingsPage extends ConsumerWidget { ); } - void _showSpotifyCredentialsDialog(BuildContext context, WidgetRef ref, AppSettings settings) { - final clientIdController = TextEditingController(text: settings.spotifyClientId); - final clientSecretController = TextEditingController(text: settings.spotifyClientSecret); + void _showSpotifyCredentialsDialog( + BuildContext context, + WidgetRef ref, + AppSettings settings, + ) { + final clientIdController = TextEditingController( + text: settings.spotifyClientId, + ); + final clientSecretController = TextEditingController( + text: settings.spotifyClientSecret, + ); final colorScheme = Theme.of(context).colorScheme; showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(28))), + backgroundColor: colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), builder: (context) => Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), child: SingleChildScrollView( child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - Center(child: Container(width: 40, height: 4, decoration: BoxDecoration(color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(2)))), - Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), - child: Text('Spotify API Credentials', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - 'Use your own credentials to avoid rate limiting.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextField( + Text( + 'Spotify Credentials', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Enter your Client ID and Secret to use your own Spotify application quota.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // Client ID + TextField( controller: clientIdController, decoration: InputDecoration( labelText: 'Client ID', - hintText: 'Enter Spotify Client ID', + hintText: 'Paste Client ID', filled: true, - fillColor: colorScheme.surfaceContainerLow, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.outline.withValues(alpha: 0.3))), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.outline.withValues(alpha: 0.3))), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.primary, width: 2)), + fillColor: colorScheme.surfaceContainerHighest.withValues( + alpha: 0.3, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: colorScheme.primary, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + prefixIcon: const Icon(Icons.person_outline), ), ), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextField( + const SizedBox(height: 16), + + // Client Secret + TextField( controller: clientSecretController, obscureText: true, decoration: InputDecoration( labelText: 'Client Secret', - hintText: 'Enter Spotify Client Secret', + hintText: 'Paste Client Secret', filled: true, - fillColor: colorScheme.surfaceContainerLow, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.outline.withValues(alpha: 0.3))), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.outline.withValues(alpha: 0.3))), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: colorScheme.primary, width: 2)), - ), - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Row( - children: [ - if (settings.spotifyClientId.isNotEmpty) - Expanded( - child: OutlinedButton( - onPressed: () { - ref.read(settingsProvider.notifier).clearSpotifyCredentials(); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials cleared')), - ); - }, - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.error, - side: BorderSide(color: colorScheme.error), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - minimumSize: const Size.fromHeight(52), - ), - child: const Text('Clear'), - ), - ), - if (settings.spotifyClientId.isNotEmpty) const SizedBox(width: 12), - Expanded( - child: FilledButton( - onPressed: () { - final clientId = clientIdController.text.trim(); - final clientSecret = clientSecretController.text.trim(); - - if (clientId.isNotEmpty && clientSecret.isNotEmpty) { - ref.read(settingsProvider.notifier).setSpotifyCredentials(clientId, clientSecret); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Credentials saved')), - ); - } else if (clientId.isEmpty && clientSecret.isEmpty) { - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please fill both Client ID and Secret')), - ); - } - }, - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - minimumSize: const Size.fromHeight(52), - ), - child: const Text('Save'), + fillColor: colorScheme.surfaceContainerHighest.withValues( + alpha: 0.3, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: colorScheme.primary, + width: 2, ), ), - ], + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + prefixIcon: const Icon(Icons.lock_outline), + ), ), - ), - ], + + const SizedBox(height: 32), + + FilledButton( + onPressed: () { + final clientId = clientIdController.text.trim(); + final clientSecret = clientSecretController.text.trim(); + + if (clientId.isNotEmpty && clientSecret.isNotEmpty) { + ref + .read(settingsProvider.notifier) + .setSpotifyCredentials(clientId, clientSecret); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Credentials saved')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please fill all fields'), + ), + ); + } + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text( + 'Save Credentials', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + + if (settings.spotifyClientId.isNotEmpty) ...[ + const SizedBox(height: 12), + TextButton( + onPressed: () { + ref + .read(settingsProvider.notifier) + .clearSpotifyCredentials(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Credentials cleared')), + ); + }, + style: TextButton.styleFrom( + foregroundColor: colorScheme.error, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text('Remove Credentials'), + ), + ], + + const SizedBox(height: 16), + ], + ), ), ), ), @@ -350,40 +470,92 @@ class OptionsSettingsPage extends ConsumerWidget { class _ConcurrentDownloadsItem extends StatelessWidget { final int currentValue; final ValueChanged onChanged; - const _ConcurrentDownloadsItem({required this.currentValue, required this.onChanged}); + const _ConcurrentDownloadsItem({ + required this.currentValue, + required this.onChanged, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.all(20), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Icon(Icons.download_for_offline, color: colorScheme.onSurfaceVariant, size: 24), - const SizedBox(width: 16), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Concurrent Downloads', style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 2), - Text(currentValue == 1 ? 'Sequential (1 at a time)' : '$currentValue parallel downloads', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), - ])), - ]), - const SizedBox(height: 16), - Row(children: [ - _ConcurrentChip(label: '1', isSelected: currentValue == 1, onTap: () => onChanged(1)), - const SizedBox(width: 8), - _ConcurrentChip(label: '2', isSelected: currentValue == 2, onTap: () => onChanged(2)), - const SizedBox(width: 8), - _ConcurrentChip(label: '3', isSelected: currentValue == 3, onTap: () => onChanged(3)), - ]), - const SizedBox(height: 12), - Row(children: [ - Icon(Icons.warning_amber_rounded, size: 16, color: colorScheme.error), - const SizedBox(width: 8), - Expanded(child: Text('Parallel downloads may trigger rate limiting', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.error))), - ]), - ]), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.download_for_offline, + color: colorScheme.onSurfaceVariant, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Concurrent Downloads', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 2), + Text( + currentValue == 1 + ? 'Sequential (1 at a time)' + : '$currentValue parallel downloads', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _ConcurrentChip( + label: '1', + isSelected: currentValue == 1, + onTap: () => onChanged(1), + ), + const SizedBox(width: 8), + _ConcurrentChip( + label: '2', + isSelected: currentValue == 2, + onTap: () => onChanged(2), + ), + const SizedBox(width: 8), + _ConcurrentChip( + label: '3', + isSelected: currentValue == 3, + onTap: () => onChanged(3), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + Icons.warning_amber_rounded, + size: 16, + color: colorScheme.error, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Parallel downloads may trigger rate limiting', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colorScheme.error), + ), + ), + ], + ), + ], + ), ); } } @@ -392,17 +564,24 @@ class _ConcurrentChip extends StatelessWidget { final String label; final bool isSelected; final VoidCallback onTap; - const _ConcurrentChip({required this.label, required this.isSelected, required this.onTap}); + const _ConcurrentChip({ + required this.label, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface) + + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) : colorScheme.surfaceContainerHigh; - + return Expanded( child: Material( color: isSelected ? colorScheme.primaryContainer : unselectedColor, @@ -412,9 +591,17 @@ class _ConcurrentChip extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Center(child: Text(label, style: TextStyle( - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant))), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), + ), ), ), ), @@ -425,38 +612,86 @@ class _ConcurrentChip extends StatelessWidget { class _UpdateChannelSelector extends StatelessWidget { final String currentChannel; final ValueChanged onChanged; - const _UpdateChannelSelector({required this.currentChannel, required this.onChanged}); + const _UpdateChannelSelector({ + required this.currentChannel, + required this.onChanged, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.all(20), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Icon(Icons.new_releases, color: colorScheme.onSurfaceVariant, size: 24), - const SizedBox(width: 16), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Update Channel', style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 2), - Text(currentChannel == 'preview' ? 'Get preview releases' : 'Stable releases only', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), - ])), - ]), - const SizedBox(height: 16), - Row(children: [ - _ChannelChip(label: 'Stable', isSelected: currentChannel == 'stable', onTap: () => onChanged('stable')), - const SizedBox(width: 8), - _ChannelChip(label: 'Preview', isSelected: currentChannel == 'preview', onTap: () => onChanged('preview')), - ]), - const SizedBox(height: 12), - Row(children: [ - Icon(Icons.info_outline, size: 16, color: colorScheme.onSurfaceVariant), - const SizedBox(width: 8), - Expanded(child: Text('Preview may contain bugs or incomplete features', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant))), - ]), - ]), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.new_releases, + color: colorScheme.onSurfaceVariant, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Update Channel', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 2), + Text( + currentChannel == 'preview' + ? 'Get preview releases' + : 'Stable releases only', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _ChannelChip( + label: 'Stable', + isSelected: currentChannel == 'stable', + onTap: () => onChanged('stable'), + ), + const SizedBox(width: 8), + _ChannelChip( + label: 'Preview', + isSelected: currentChannel == 'preview', + onTap: () => onChanged('preview'), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Preview may contain bugs or incomplete features', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ], + ), ); } } @@ -465,17 +700,24 @@ class _ChannelChip extends StatelessWidget { final String label; final bool isSelected; final VoidCallback onTap; - const _ChannelChip({required this.label, required this.isSelected, required this.onTap}); + const _ChannelChip({ + required this.label, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface) + + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) : colorScheme.surfaceContainerHigh; - + return Expanded( child: Material( color: isSelected ? colorScheme.primaryContainer : unselectedColor, @@ -485,9 +727,17 @@ class _ChannelChip extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Center(child: Text(label, style: TextStyle( - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant))), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), + ), ), ), ), @@ -498,57 +748,81 @@ class _ChannelChip extends StatelessWidget { class _MetadataSourceSelector extends StatelessWidget { final String currentSource; final ValueChanged onChanged; - const _MetadataSourceSelector({required this.currentSource, required this.onChanged}); + const _MetadataSourceSelector({ + required this.currentSource, + required this.onChanged, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.all(20), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Icon(Icons.search, color: colorScheme.onSurfaceVariant, size: 24), - const SizedBox(width: 16), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Search Source', style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 2), - Text(currentSource == 'deezer' ? 'Deezer (no need developer account)' : 'Spotify (may hit rate limit)', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), - ])), - ]), - const SizedBox(height: 16), - Row(children: [ - _SourceChip(label: 'Deezer', isSelected: currentSource == 'deezer', onTap: () => onChanged('deezer')), - const SizedBox(width: 8), - _SourceChip(label: 'Spotify', isSelected: currentSource == 'spotify', onTap: () => onChanged('spotify')), - ]), - const SizedBox(height: 12), - Row(children: [ - Icon(Icons.info_outline, size: 16, color: colorScheme.onSurfaceVariant), - const SizedBox(width: 8), - Expanded(child: Text('Spotify URLs are always supported regardless of this setting', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant))), - ]), - ]), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Primary Provider', + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text( + 'Service used when searching by track name.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + _SourceChip( + icon: Icons.graphic_eq, + label: 'Deezer', + isSelected: currentSource == 'deezer', + onTap: () => onChanged('deezer'), + ), + const SizedBox(width: 12), + _SourceChip( + icon: Icons.music_note, + label: 'Spotify', + isSelected: currentSource == 'spotify', + onTap: () => onChanged('spotify'), + ), + ], + ), + ], + ), ); } } class _SourceChip extends StatelessWidget { + final IconData icon; final String label; final bool isSelected; final VoidCallback onTap; - const _SourceChip({required this.label, required this.isSelected, required this.onTap}); + + const _SourceChip({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend(Colors.white.withValues(alpha: 0.05), colorScheme.surface) + + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) : colorScheme.surfaceContainerHigh; - + return Expanded( child: Material( color: isSelected ? colorScheme.primaryContainer : unselectedColor, @@ -557,10 +831,31 @@ class _SourceChip extends StatelessWidget { onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Center(child: Text(label, style: TextStyle( - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant))), + padding: const EdgeInsets.symmetric(vertical: 14), + child: Column( + children: [ + Icon( + icon, + size: 28, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ), ), diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index f9dc3014..619abf83 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -7,11 +7,9 @@ class AppTheme { static const Color defaultSeedColor = Color(kDefaultSeedColor); /// Create light theme - static ThemeData light({ - ColorScheme? dynamicScheme, - Color? seedColor, - }) { - final scheme = dynamicScheme ?? + static ThemeData light({ColorScheme? dynamicScheme, Color? seedColor}) { + final scheme = + dynamicScheme ?? ColorScheme.fromSeed( seedColor: seedColor ?? defaultSeedColor, brightness: Brightness.light, @@ -45,7 +43,8 @@ class AppTheme { Color? seedColor, bool isAmoled = false, }) { - final scheme = dynamicScheme ?? + final scheme = + dynamicScheme ?? ColorScheme.fromSeed( seedColor: seedColor ?? defaultSeedColor, brightness: Brightness.dark, @@ -75,34 +74,41 @@ class AppTheme { } /// AppBar theme - static AppBarTheme _appBarTheme(ColorScheme scheme, {bool isAmoled = false}) => AppBarTheme( - elevation: 0, - scrolledUnderElevation: isAmoled ? 0 : 3, - backgroundColor: isAmoled ? Colors.black : scheme.surface, - foregroundColor: scheme.onSurface, - surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint, - centerTitle: true, - titleTextStyle: TextStyle( - color: scheme.onSurface, - fontSize: 22, - fontWeight: FontWeight.w500, - ), - ); + static AppBarTheme _appBarTheme( + ColorScheme scheme, { + bool isAmoled = false, + }) => AppBarTheme( + elevation: 0, + scrolledUnderElevation: isAmoled ? 0 : 3, + backgroundColor: isAmoled ? Colors.black : scheme.surface, + foregroundColor: scheme.onSurface, + surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint, + centerTitle: true, + titleTextStyle: TextStyle( + color: scheme.onSurface, + fontSize: 22, + fontWeight: FontWeight.w500, + ), + ); /// Card theme static CardThemeData _cardTheme(ColorScheme scheme) => CardThemeData( - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - color: scheme.surfaceContainerLow, - surfaceTintColor: scheme.surfaceTint, - ); + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 12 -> 16 + color: scheme.surfaceContainerLow, + surfaceTintColor: scheme.surfaceTint, + ); /// Elevated button theme static ElevatedButtonThemeData _elevatedButtonTheme(ColorScheme scheme) => ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 1, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 20 -> 16 padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); @@ -111,7 +117,9 @@ class AppTheme { static FilledButtonThemeData _filledButtonTheme(ColorScheme scheme) => FilledButtonThemeData( style: FilledButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 20 -> 16 padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); @@ -120,7 +128,9 @@ class AppTheme { static OutlinedButtonThemeData _outlinedButtonTheme(ColorScheme scheme) => OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 20 -> 16 padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ); @@ -129,7 +139,9 @@ class AppTheme { static TextButtonThemeData _textButtonTheme(ColorScheme scheme) => TextButtonThemeData( style: TextButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 20 -> 16 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ); @@ -147,52 +159,63 @@ class AppTheme { static InputDecorationTheme _inputDecorationTheme(ColorScheme scheme) => InputDecorationTheme( filled: true, - fillColor: scheme.surfaceContainerHighest, + fillColor: scheme.surfaceContainerHighest.withValues( + alpha: 0.3, + ), // Added transparency border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), // 12 -> 16 borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), // 12 -> 16 borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), // 12 -> 16 borderSide: BorderSide(color: scheme.primary, width: 2), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), // 12 -> 16 borderSide: BorderSide(color: scheme.error, width: 1), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), // consistent padding ); /// List tile theme - static ListTileThemeData _listTileTheme(ColorScheme scheme) => ListTileThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + static ListTileThemeData _listTileTheme(ColorScheme scheme) => + ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // 12 -> 16 contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), ); /// Dialog theme static DialogThemeData _dialogTheme(ColorScheme scheme) => DialogThemeData( - elevation: 6, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), - backgroundColor: scheme.surfaceContainerHigh, - surfaceTintColor: scheme.surfaceTint, - ); + elevation: 6, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + backgroundColor: scheme.surfaceContainerHigh, + surfaceTintColor: scheme.surfaceTint, + ); /// Navigation bar theme - static NavigationBarThemeData _navigationBarTheme(ColorScheme scheme, {bool isAmoled = false}) => - NavigationBarThemeData( - elevation: 0, - backgroundColor: isAmoled ? Colors.black : scheme.surfaceContainer, - indicatorColor: scheme.secondaryContainer, - surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint, - labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, - ); + static NavigationBarThemeData _navigationBarTheme( + ColorScheme scheme, { + bool isAmoled = false, + }) => NavigationBarThemeData( + elevation: 0, + backgroundColor: isAmoled ? Colors.black : scheme.surfaceContainer, + indicatorColor: scheme.secondaryContainer, + surfaceTintColor: isAmoled ? Colors.transparent : scheme.surfaceTint, + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, + ); /// SnackBar theme - static SnackBarThemeData _snackBarTheme(ColorScheme scheme) => SnackBarThemeData( + static SnackBarThemeData _snackBarTheme(ColorScheme scheme) => + SnackBarThemeData( behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), backgroundColor: scheme.inverseSurface, @@ -200,46 +223,44 @@ class AppTheme { ); /// Progress indicator theme - static ProgressIndicatorThemeData _progressIndicatorTheme(ColorScheme scheme) => - ProgressIndicatorThemeData( - color: scheme.primary, - linearTrackColor: scheme.surfaceContainerHighest, - circularTrackColor: scheme.surfaceContainerHighest, - ); + static ProgressIndicatorThemeData _progressIndicatorTheme( + ColorScheme scheme, + ) => ProgressIndicatorThemeData( + color: scheme.primary, + linearTrackColor: scheme.surfaceContainerHighest, + circularTrackColor: scheme.surfaceContainerHighest, + ); /// Switch theme static SwitchThemeData _switchTheme(ColorScheme scheme) => SwitchThemeData( - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return scheme.onPrimary; - } - return scheme.outline; - }), - trackColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return scheme.primary; - } - return scheme.surfaceContainerHighest; - }), - thumbIcon: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return Icon(Icons.check, color: scheme.primary); - } - return null; - }), - ); + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return scheme.onPrimary; + } + return scheme.outline; + }), + trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return scheme.primary; + } + return scheme.surfaceContainerHighest; + }), + thumbIcon: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Icon(Icons.check, color: scheme.primary); + } + return null; + }), + ); /// Chip theme static ChipThemeData _chipTheme(ColorScheme scheme) => ChipThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - backgroundColor: scheme.surfaceContainerLow, - selectedColor: scheme.secondaryContainer, - ); + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + backgroundColor: scheme.surfaceContainerLow, + selectedColor: scheme.secondaryContainer, + ); /// Divider theme - static DividerThemeData _dividerTheme(ColorScheme scheme) => DividerThemeData( - color: scheme.outlineVariant, - thickness: 1, - space: 1, - ); + static DividerThemeData _dividerTheme(ColorScheme scheme) => + DividerThemeData(color: scheme.outlineVariant, thickness: 1, space: 1); }