mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
feat(material): unify search filters and extension actions
This commit is contained in:
1 parent
fb13b87561
commit
892911c825
12 files changed
+71
-46
No files matched your search
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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')],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)'));
|
||||||
|
|||||||
Reference in new issue
Block a user