feat(material): unify search filters and extension actions

This commit is contained in:
zarzet committed 2026-09-28 11:58:49 +07:00
1 parent fb13b87561
commit 892911c825
12 files changed
+71 -46

No files matched your search

+3
View File
@@ -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/app_tokens.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/app_switch.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/theme/mornye_icons.dart';
import 'package:spotiflac_android/widgets/mornye_chrome.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart';
import 'package:spotiflac_android/widgets/mornye_artist_header.dart'; import 'package:spotiflac_android/widgets/mornye_artist_header.dart';
+1 -1
View File
@@ -29,7 +29,7 @@ extension _HomeTabCsvImport on _HomeTabState {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const CircularProgressIndicator(), const AppLoadingIndicator(),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
totalTracks > 0 totalTracks > 0
+6 -6
View File
@@ -226,10 +226,10 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
onTap: () => setSheetState(() => tempSort = option), onTap: () => setSheetState(() => tempSort = option),
); );
} }
return FilterChip( return AppChoiceChip(
label: Text(_sortOptionLabel(option)), label: Text(_sortOptionLabel(option)),
selected: tempSort == option, selected: tempSort == option,
showCheckmark: false, singleChoice: true,
onSelected: (_) => onSelected: (_) =>
setSheetState(() => tempSort = option), setSheetState(() => tempSort = option),
); );
@@ -238,7 +238,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: ExpressiveButton(
onPressed: () { onPressed: () {
Navigator.pop(ctx); Navigator.pop(ctx);
if (_searchSortOption != tempSort) { if (_searchSortOption != tempSort) {
@@ -731,12 +731,12 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
onTap: select, onTap: select,
), ),
) )
: FilterChip( : AppChoiceChip(
label: Text(label), label: Text(label),
selected: selectedFilter == id, selected: selectedFilter == id,
onSelected: (_) => select(), onSelected: (_) => select(),
showCheckmark: false, singleChoice: true,
avatar: icon == null ? null : Icon(icon, size: 18), icon: icon == null ? null : Icon(icon, size: 18),
); );
} }
+1 -8
View File
@@ -34,14 +34,7 @@ class _SearchProviderDropdown extends ConsumerWidget {
height: 28, height: 28,
child: Center( child: Center(
child: isProviderLoading child: isProviderLoading
? SizedBox( ? AppLoadingIndicator(size: 18, color: colorScheme.primary)
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colorScheme.primary,
),
)
: Icon( : Icon(
Icons.search_off, Icons.search_off,
size: 20, size: 20,
-1
View File
@@ -24,7 +24,6 @@ import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart'; import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.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/file_access.dart';
import 'package:spotiflac_android/utils/ordered_range_selection.dart'; import 'package:spotiflac_android/utils/ordered_range_selection.dart';
import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/models/download_item.dart';
+4 -7
View File
@@ -137,15 +137,12 @@ class _FilterChip extends ConsumerWidget {
); );
} }
return FilterChip( return AppChoiceChip(
label: content, label: Text(label),
count: count,
singleChoice: true,
selected: isSelected, selected: isSelected,
onSelected: (_) => onTap(), onSelected: (_) => onTap(),
showCheckmark: false,
backgroundColor: settingsGroupColor(context),
side: BorderSide(
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
),
); );
} }
} }
+30 -13
View File
@@ -20,6 +20,9 @@ import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/theme/mornye_icons.dart'; import 'package:spotiflac_android/theme/mornye_icons.dart';
import 'package:spotiflac_android/widgets/mornye_chrome.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart';
import 'package:spotiflac_android/widgets/extension_repo_card.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 { class RepoTab extends ConsumerStatefulWidget {
const RepoTab({super.key}); const RepoTab({super.key});
@@ -703,18 +706,12 @@ class _CategoryChip extends ConsumerWidget {
), ),
); );
} }
return FilterChip( return AppChoiceChip(
label: Row( label: Text(label),
mainAxisSize: MainAxisSize.min, icon: Icon(icon, size: 18),
children: [Icon(icon, size: 16), const SizedBox(width: 6), Text(label)], singleChoice: true,
),
selected: isSelected, selected: isSelected,
onSelected: (_) => onTap(), 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( trailing: Padding(
padding: EdgeInsets.only(left: tokens.gapMd), padding: EdgeInsets.only(left: tokens.gapMd),
child: isDownloading child: isDownloading
? const SizedBox.square( ? const AppLoadingIndicator(size: 24)
dimension: 24, : !context.isMornye
child: CircularProgressIndicator(strokeWidth: 2), ? 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 : extension.hasUpdate
? FilledButton.tonal( ? FilledButton.tonal(
+1 -7
View File
@@ -2462,7 +2462,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
}), }),
); );
} }
return FilterChip( return AppChoiceChip(
label: Text(_fieldLabel(key)), label: Text(_fieldLabel(key)),
selected: selected, selected: selected,
onSelected: _fetching 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(), }).toList(),
), ),
+1
View File
@@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'
CupertinoTextField, CupertinoTextField,
CupertinoActivityIndicator; CupertinoActivityIndicator;
import 'package:spotiflac_android/widgets/app_action_button.dart'; 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_switch.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
+18 -2
View File
@@ -13,6 +13,8 @@ class AppChoiceChip extends StatelessWidget {
required this.onSelected, required this.onSelected,
this.visualDensity, this.visualDensity,
this.singleChoice = false, this.singleChoice = false,
this.icon,
this.count,
}); });
final Widget label; final Widget label;
@@ -20,6 +22,8 @@ class AppChoiceChip extends StatelessWidget {
final ValueChanged<bool>? onSelected; final ValueChanged<bool>? onSelected;
final VisualDensity? visualDensity; final VisualDensity? visualDensity;
final bool singleChoice; final bool singleChoice;
final Widget? icon;
final int? count;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -28,6 +32,8 @@ class AppChoiceChip extends StatelessWidget {
return MaterialExpressiveScope( return MaterialExpressiveScope(
child: M3EChip( child: M3EChip(
label: text.data ?? text.textSpan?.toPlainText() ?? '', label: text.data ?? text.textSpan?.toPlainText() ?? '',
leading: icon,
trailing: count == null ? null : Text('$count'),
type: M3EChipType.filter, type: M3EChipType.filter,
selected: selected, selected: selected,
onPressed: onSelected == null ? null : () => onSelected!(!selected), onPressed: onSelected == null ? null : () => onSelected!(!selected),
@@ -35,16 +41,24 @@ class AppChoiceChip extends StatelessWidget {
); );
} }
if (!context.isMornye) { if (!context.isMornye) {
final content = count == null
? label
: Row(
mainAxisSize: MainAxisSize.min,
children: [label, const SizedBox(width: 6), Text('$count')],
);
if (singleChoice) { if (singleChoice) {
return ChoiceChip( return ChoiceChip(
label: label, label: content,
avatar: icon,
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
visualDensity: visualDensity, visualDensity: visualDensity,
); );
} }
return FilterChip( return FilterChip(
label: label, label: content,
avatar: icon,
selected: selected, selected: selected,
onSelected: onSelected, onSelected: onSelected,
visualDensity: visualDensity, visualDensity: visualDensity,
@@ -74,11 +88,13 @@ class AppChoiceChip extends StatelessWidget {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (icon != null) ...[icon!, const SizedBox(width: 8)],
if (selected) ...[ if (selected) ...[
Icon(CupertinoIcons.check_mark, size: 18, color: foreground), Icon(CupertinoIcons.check_mark, size: 18, color: foreground),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
Flexible(child: label), Flexible(child: label),
if (count != null) ...[const SizedBox(width: 6), Text('$count')],
], ],
), ),
), ),
+4
View File
@@ -145,6 +145,8 @@ void main() {
StatefulBuilder( StatefulBuilder(
builder: (context, setState) => AppChoiceChip( builder: (context, setState) => AppChoiceChip(
label: const Text('Albums'), label: const Text('Albums'),
icon: const Icon(Icons.album_outlined),
count: 12,
selected: selected, selected: selected,
singleChoice: true, singleChoice: true,
onSelected: (next) => setState(() => selected = next), onSelected: (next) => setState(() => selected = next),
@@ -153,6 +155,8 @@ void main() {
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('12'), findsOneWidget);
expect(find.byIcon(Icons.album_outlined), findsOneWidget);
await tester.tap(find.text('Albums')); await tester.tap(find.text('Albums'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(selected, isTrue); expect(selected, isTrue);
+2 -1
View File
@@ -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/providers/extension_provider.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart'; import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/widgets/app_choice_chip.dart';
class _Extensions extends ExtensionNotifier { class _Extensions extends ExtensionNotifier {
@override @override
@@ -171,7 +172,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find.text('None')); await tester.tap(find.text('None'));
await tester.pump(); await tester.pump();
await tester.tap(find.widgetWithText(FilterChip, 'Cover Art')); await tester.tap(find.widgetWithText(AppChoiceChip, 'Cover Art'));
await tester.pump(); await tester.pump();
if (scenario == 'selected provider album') { if (scenario == 'selected provider album') {
await tester.tap(find.text('Automatic (provider priority)')); await tester.tap(find.text('Automatic (provider priority)'));