diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 298b3e67..9892e424 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -6,6 +6,9 @@ import 'package:spotiflac_android/screens/track_history_snapshot.dart'; import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/widgets/app_switch.dart'; +import 'package:spotiflac_android/widgets/app_choice_chip.dart'; +import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; +import 'package:spotiflac_android/widgets/expressive_button.dart'; import 'package:spotiflac_android/theme/mornye_icons.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; import 'package:spotiflac_android/widgets/mornye_artist_header.dart'; diff --git a/lib/screens/home_tab_import.dart b/lib/screens/home_tab_import.dart index 845af69f..41c7d370 100644 --- a/lib/screens/home_tab_import.dart +++ b/lib/screens/home_tab_import.dart @@ -29,7 +29,7 @@ extension _HomeTabCsvImport on _HomeTabState { content: Column( mainAxisSize: MainAxisSize.min, children: [ - const CircularProgressIndicator(), + const AppLoadingIndicator(), const SizedBox(height: 16), Text( totalTracks > 0 diff --git a/lib/screens/home_tab_search_results.dart b/lib/screens/home_tab_search_results.dart index 8d600dc4..fd94ef38 100644 --- a/lib/screens/home_tab_search_results.dart +++ b/lib/screens/home_tab_search_results.dart @@ -226,10 +226,10 @@ extension _HomeTabSearchResultsUI on _HomeTabState { onTap: () => setSheetState(() => tempSort = option), ); } - return FilterChip( + return AppChoiceChip( label: Text(_sortOptionLabel(option)), selected: tempSort == option, - showCheckmark: false, + singleChoice: true, onSelected: (_) => setSheetState(() => tempSort = option), ); @@ -238,7 +238,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState { const SizedBox(height: 24), SizedBox( width: double.infinity, - child: FilledButton( + child: ExpressiveButton( onPressed: () { Navigator.pop(ctx); if (_searchSortOption != tempSort) { @@ -731,12 +731,12 @@ extension _HomeTabSearchResultsUI on _HomeTabState { onTap: select, ), ) - : FilterChip( + : AppChoiceChip( label: Text(label), selected: selectedFilter == id, onSelected: (_) => select(), - showCheckmark: false, - avatar: icon == null ? null : Icon(icon, size: 18), + singleChoice: true, + icon: icon == null ? null : Icon(icon, size: 18), ); } diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index edba0c8c..9b0d8657 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -34,14 +34,7 @@ class _SearchProviderDropdown extends ConsumerWidget { height: 28, child: Center( child: isProviderLoading - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: colorScheme.primary, - ), - ) + ? AppLoadingIndicator(size: 18, color: colorScheme.primary) : Icon( Icons.search_off, size: 20, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index d4a08b11..06bc95f9 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -24,7 +24,6 @@ import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; -import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/ordered_range_selection.dart'; import 'package:spotiflac_android/models/download_item.dart'; diff --git a/lib/screens/queue_tab_widgets.dart b/lib/screens/queue_tab_widgets.dart index 62bc9292..190debe6 100644 --- a/lib/screens/queue_tab_widgets.dart +++ b/lib/screens/queue_tab_widgets.dart @@ -137,15 +137,12 @@ class _FilterChip extends ConsumerWidget { ); } - return FilterChip( - label: content, + return AppChoiceChip( + label: Text(label), + count: count, + singleChoice: true, selected: isSelected, onSelected: (_) => onTap(), - showCheckmark: false, - backgroundColor: settingsGroupColor(context), - side: BorderSide( - color: colorScheme.outlineVariant.withValues(alpha: 0.6), - ), ); } } diff --git a/lib/screens/repo_tab.dart b/lib/screens/repo_tab.dart index 7a1cd932..3cc2ed9f 100644 --- a/lib/screens/repo_tab.dart +++ b/lib/screens/repo_tab.dart @@ -20,6 +20,9 @@ import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/theme/mornye_icons.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; import 'package:spotiflac_android/widgets/extension_repo_card.dart'; +import 'package:spotiflac_android/widgets/app_choice_chip.dart'; +import 'package:spotiflac_android/widgets/expressive_button.dart'; +import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; class RepoTab extends ConsumerStatefulWidget { const RepoTab({super.key}); @@ -703,18 +706,12 @@ class _CategoryChip extends ConsumerWidget { ), ); } - return FilterChip( - label: Row( - mainAxisSize: MainAxisSize.min, - children: [Icon(icon, size: 16), const SizedBox(width: 6), Text(label)], - ), + return AppChoiceChip( + label: Text(label), + icon: Icon(icon, size: 18), + singleChoice: true, selected: isSelected, onSelected: (_) => onTap(), - showCheckmark: false, - backgroundColor: settingsGroupColor(context), - side: BorderSide( - color: colorScheme.outlineVariant.withValues(alpha: 0.6), - ), ); } } @@ -830,9 +827,29 @@ class _ExtensionItem extends StatelessWidget { trailing: Padding( padding: EdgeInsets.only(left: tokens.gapMd), child: isDownloading - ? const SizedBox.square( - dimension: 24, - child: CircularProgressIndicator(strokeWidth: 2), + ? const AppLoadingIndicator(size: 24) + : !context.isMornye + ? ExpressiveButton( + onPressed: extension.hasUpdate + ? onUpdate + : extension.isInstalled + ? null + : onInstall, + outlined: extension.isInstalled && !extension.hasUpdate, + tonal: extension.hasUpdate, + icon: extension.isInstalled && !extension.hasUpdate + ? const Icon(Icons.check, size: 16) + : null, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 16), + ), + child: Text( + extension.hasUpdate + ? context.l10n.storeUpdate + : extension.isInstalled + ? context.l10n.storeInstalled + : context.l10n.storeInstall, + ), ) : extension.hasUpdate ? FilledButton.tonal( diff --git a/lib/screens/track_metadata_edit_sheet.dart b/lib/screens/track_metadata_edit_sheet.dart index f53c700e..b38d9e40 100644 --- a/lib/screens/track_metadata_edit_sheet.dart +++ b/lib/screens/track_metadata_edit_sheet.dart @@ -2462,7 +2462,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { }), ); } - return FilterChip( + return AppChoiceChip( label: Text(_fieldLabel(key)), selected: selected, onSelected: _fetching @@ -2477,12 +2477,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } }); }, - backgroundColor: cs.surfaceContainerHigh, - selectedColor: cs.primaryContainer, - checkmarkColor: cs.onPrimaryContainer, - labelStyle: Theme.of(context).textTheme.labelMedium?.copyWith( - color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, - ), ); }).toList(), ), diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 1b1b54ce..ea67002e 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart' CupertinoTextField, CupertinoActivityIndicator; import 'package:spotiflac_android/widgets/app_action_button.dart'; +import 'package:spotiflac_android/widgets/app_choice_chip.dart'; import 'package:spotiflac_android/widgets/app_switch.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:flutter/services.dart'; diff --git a/lib/widgets/app_choice_chip.dart b/lib/widgets/app_choice_chip.dart index 35ca8018..363c8e7b 100644 --- a/lib/widgets/app_choice_chip.dart +++ b/lib/widgets/app_choice_chip.dart @@ -13,6 +13,8 @@ class AppChoiceChip extends StatelessWidget { required this.onSelected, this.visualDensity, this.singleChoice = false, + this.icon, + this.count, }); final Widget label; @@ -20,6 +22,8 @@ class AppChoiceChip extends StatelessWidget { final ValueChanged? onSelected; final VisualDensity? visualDensity; final bool singleChoice; + final Widget? icon; + final int? count; @override Widget build(BuildContext context) { @@ -28,6 +32,8 @@ class AppChoiceChip extends StatelessWidget { return MaterialExpressiveScope( child: M3EChip( label: text.data ?? text.textSpan?.toPlainText() ?? '', + leading: icon, + trailing: count == null ? null : Text('$count'), type: M3EChipType.filter, selected: selected, onPressed: onSelected == null ? null : () => onSelected!(!selected), @@ -35,16 +41,24 @@ class AppChoiceChip extends StatelessWidget { ); } if (!context.isMornye) { + final content = count == null + ? label + : Row( + mainAxisSize: MainAxisSize.min, + children: [label, const SizedBox(width: 6), Text('$count')], + ); if (singleChoice) { return ChoiceChip( - label: label, + label: content, + avatar: icon, selected: selected, onSelected: onSelected, visualDensity: visualDensity, ); } return FilterChip( - label: label, + label: content, + avatar: icon, selected: selected, onSelected: onSelected, visualDensity: visualDensity, @@ -74,11 +88,13 @@ class AppChoiceChip extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ + if (icon != null) ...[icon!, const SizedBox(width: 8)], if (selected) ...[ Icon(CupertinoIcons.check_mark, size: 18, color: foreground), const SizedBox(width: 8), ], Flexible(child: label), + if (count != null) ...[const SizedBox(width: 6), Text('$count')], ], ), ), diff --git a/test/material_expressive_test.dart b/test/material_expressive_test.dart index 2d5274a5..24037ba9 100644 --- a/test/material_expressive_test.dart +++ b/test/material_expressive_test.dart @@ -145,6 +145,8 @@ void main() { StatefulBuilder( builder: (context, setState) => AppChoiceChip( label: const Text('Albums'), + icon: const Icon(Icons.album_outlined), + count: 12, selected: selected, singleChoice: true, onSelected: (next) => setState(() => selected = next), @@ -153,6 +155,8 @@ void main() { ), ); await tester.pumpAndSettle(); + expect(find.text('12'), findsOneWidget); + expect(find.byIcon(Icons.album_outlined), findsOneWidget); await tester.tap(find.text('Albums')); await tester.pumpAndSettle(); expect(selected, isTrue); diff --git a/test/track_metadata_cover_search_test.dart b/test/track_metadata_cover_search_test.dart index 015f7310..5181a18f 100644 --- a/test/track_metadata_cover_search_test.dart +++ b/test/track_metadata_cover_search_test.dart @@ -12,6 +12,7 @@ import 'package:spotiflac_android/providers/download_history_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/widgets/app_choice_chip.dart'; class _Extensions extends ExtensionNotifier { @override @@ -171,7 +172,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.text('None')); await tester.pump(); - await tester.tap(find.widgetWithText(FilterChip, 'Cover Art')); + await tester.tap(find.widgetWithText(AppChoiceChip, 'Cover Art')); await tester.pump(); if (scenario == 'selected provider album') { await tester.tap(find.text('Automatic (provider priority)'));