mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
fix(settings): unify adaptive sheet layout and drag behavior
This commit is contained in:
1 parent
c42b314042
commit
1cc233bdc1
12 files changed
+880
-944
No files matched your search
@@ -30,7 +30,6 @@ const _alphabeticalSorts = {'a-z', 'z-a'};
|
||||
const _artistSorts = {'artist-asc', 'artist-desc'};
|
||||
const _albumSorts = {'album-asc', 'album-desc'};
|
||||
const _dateSorts = {'latest', 'oldest'};
|
||||
const _libraryOptionsSheetHeightFactor = 0.7;
|
||||
|
||||
/// Sort choices offered by the Library sort sheet, in display order.
|
||||
const _librarySortModes = [
|
||||
@@ -207,14 +206,7 @@ extension _QueueTabSections on _QueueTabState {
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: _libraryOptionsSheetHeightFactor,
|
||||
maxChildSize: _libraryOptionsSheetHeightFactor,
|
||||
minChildSize: 0.3,
|
||||
snap: true,
|
||||
builder: builder,
|
||||
),
|
||||
builder: (context) => AppScrollableSheet(builder: builder),
|
||||
);
|
||||
|
||||
Future<void> _showSortSheet(BuildContext context) async {
|
||||
|
||||
@@ -958,63 +958,39 @@ class _LanguageSelector extends StatelessWidget {
|
||||
|
||||
void _showLanguagePicker(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.l10n.appearanceLanguage,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
title: context.l10n.appearanceLanguage,
|
||||
builder: (context, controller) => ListView.builder(
|
||||
controller: controller,
|
||||
shrinkWrap: true,
|
||||
itemCount: supportedLanguageChoices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final lang = supportedLanguageChoices[index];
|
||||
final isSelected = currentLocale == lang.$1;
|
||||
return AppSheetOption(
|
||||
leading: AppIcon(
|
||||
lang.$3,
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
lang.$2,
|
||||
style: TextStyle(
|
||||
color: isSelected ? colorScheme.primary : colorScheme.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: supportedLanguageChoices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final lang = supportedLanguageChoices[index];
|
||||
final isSelected = currentLocale == lang.$1;
|
||||
return AppSheetOption(
|
||||
leading: AppIcon(
|
||||
lang.$3,
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
lang.$2,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
onChanged(lang.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
onChanged(lang.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,44 +408,46 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
String current,
|
||||
) {
|
||||
final normalizedCurrent = normalizeAutoConvertFormat(current);
|
||||
showAppBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
title: context.l10n.downloadAutoConvertFormat,
|
||||
subtitle: context.l10n.downloadAutoConvertFormatSubtitle,
|
||||
builder: (sheetContext) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final format in autoConvertFormats)
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
format == 'opus'
|
||||
? Icons.graphic_eq
|
||||
: format == 'aac'
|
||||
? Icons.album_outlined
|
||||
: Icons.audiotrack,
|
||||
builder: (sheetContext, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final format in autoConvertFormats)
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
format == 'opus'
|
||||
? Icons.graphic_eq
|
||||
: format == 'aac'
|
||||
? Icons.album_outlined
|
||||
: Icons.audiotrack,
|
||||
),
|
||||
title: Text(autoConvertFormatLabel(format)),
|
||||
subtitle: Text(switch (format) {
|
||||
'aac' => context.l10n.downloadAutoConvertM4aSubtitle,
|
||||
'opus' => context.l10n.downloadAutoConvertOpusSubtitle,
|
||||
_ => context.l10n.downloadAutoConvertMp3Subtitle,
|
||||
}),
|
||||
trailing: normalizedCurrent == format
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertFormat(format);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
title: Text(autoConvertFormatLabel(format)),
|
||||
subtitle: Text(switch (format) {
|
||||
'aac' => context.l10n.downloadAutoConvertM4aSubtitle,
|
||||
'opus' => context.l10n.downloadAutoConvertOpusSubtitle,
|
||||
_ => context.l10n.downloadAutoConvertMp3Subtitle,
|
||||
}),
|
||||
trailing: normalizedCurrent == format
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertFormat(format);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -456,33 +458,35 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
String current,
|
||||
) {
|
||||
final normalizedCurrent = normalizeAutoConvertBitrate(current);
|
||||
showAppBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
title: context.l10n.downloadAutoConvertBitrate,
|
||||
subtitle: context.l10n.downloadAutoConvertBitrateSubtitle,
|
||||
builder: (sheetContext) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final bitrate in autoConvertBitrates)
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.speed_outlined),
|
||||
title: Text(bitrate.replaceAll('k', ' kbps')),
|
||||
trailing: normalizedCurrent == bitrate
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertBitrate(bitrate);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
builder: (sheetContext, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final bitrate in autoConvertBitrates)
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.speed_outlined),
|
||||
title: Text(bitrate.replaceAll('k', ' kbps')),
|
||||
trailing: normalizedCurrent == bitrate
|
||||
? AppIcon(
|
||||
Icons.check,
|
||||
color: Theme.of(sheetContext).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAutoConvertBitrate(bitrate);
|
||||
Navigator.pop(sheetContext);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -493,33 +497,16 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.settingsDownloadNetwork,
|
||||
subtitle: context.l10n.settingsDownloadNetworkSubtitle,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.settingsDownloadNetwork,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.settingsDownloadNetworkSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.signal_cellular_alt),
|
||||
title: Text(context.l10n.settingsDownloadNetworkAny),
|
||||
@@ -561,33 +548,16 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
int current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.settingsConcurrentDownloads,
|
||||
subtitle: context.l10n.settingsConcurrentDownloadsSubtitle,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.settingsConcurrentDownloads,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.settingsConcurrentDownloadsSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final count in const [1, 2, 3])
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
@@ -809,14 +779,13 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
'ZM',
|
||||
'ZW',
|
||||
];
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final normalizedCurrent = current.trim().toUpperCase();
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _RegionPickerSheet(
|
||||
title: context.l10n.downloadSongLinkRegion,
|
||||
subtitle: context.l10n.downloadSongLinkRegionDesc,
|
||||
builder: (context, controller) => _RegionPickerSheet(
|
||||
scrollController: controller,
|
||||
regions: regions,
|
||||
selected: normalizedCurrent,
|
||||
countryNameOf: (context, code) => _regionCountryName(context, code),
|
||||
@@ -836,12 +805,14 @@ class _RegionPickerSheet extends StatefulWidget {
|
||||
required this.selected,
|
||||
required this.countryNameOf,
|
||||
required this.onSelected,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final List<String> regions;
|
||||
final String selected;
|
||||
final String Function(BuildContext context, String code) countryNameOf;
|
||||
final ValueChanged<String> onSelected;
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
State<_RegionPickerSheet> createState() => _RegionPickerSheetState();
|
||||
@@ -871,70 +842,57 @@ class _RegionPickerSheetState extends State<_RegionPickerSheet> {
|
||||
.contains(query);
|
||||
}).toList();
|
||||
|
||||
return SafeArea(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.7,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.downloadSongLinkRegion,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
|
||||
child: Text(
|
||||
context.l10n.downloadSongLinkRegionDesc,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: AppSearchField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) => setState(() => _query = value),
|
||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
||||
clearTooltip: context.l10n.dialogClear,
|
||||
onClear: () => setState(() => _query = ''),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: matches.isEmpty
|
||||
? Center(
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: AppSearchField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) => setState(() => _query = value),
|
||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
||||
clearTooltip: context.l10n.dialogClear,
|
||||
onClear: () => setState(() => _query = ''),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: matches.isEmpty
|
||||
? ListView(
|
||||
controller: widget.scrollController,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
context.l10n.searchEmptyResultSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: matches.length,
|
||||
itemBuilder: (context, index) {
|
||||
final code = matches[index];
|
||||
final isSelected = code == widget.selected;
|
||||
final countryName = widget.countryNameOf(context, code);
|
||||
return AppSheetOption(
|
||||
title: Text(countryName != code ? countryName : code),
|
||||
subtitle: countryName != code ? Text(code) : null,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () => widget.onSelected(code),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: widget.scrollController,
|
||||
shrinkWrap: true,
|
||||
itemCount: matches.length,
|
||||
itemBuilder: (context, index) {
|
||||
final code = matches[index];
|
||||
final isSelected = code == widget.selected;
|
||||
final countryName = widget.countryNameOf(context, code);
|
||||
return AppSheetOption(
|
||||
title: Text(countryName != code ? countryName : code),
|
||||
subtitle: countryName != code ? Text(code) : null,
|
||||
trailing: isSelected
|
||||
? AppIcon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () => widget.onSelected(code),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,11 +1109,11 @@ class _SettingItemState extends State<_SettingItem> {
|
||||
}
|
||||
|
||||
Future<void> _showOptions() async {
|
||||
final value = await showAppBottomSheet<String>(
|
||||
final value = await showAppScrollableBottomSheet<String>(
|
||||
context: context,
|
||||
title: widget.setting.label,
|
||||
maxHeightFactor: 0.7,
|
||||
builder: (sheetContext) => SingleChildScrollView(
|
||||
builder: (sheetContext, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:spotiflac_android/screens/settings/metadata_provider_priority_pa
|
||||
import 'package:spotiflac_android/screens/settings/provider_priority_page.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/app_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/app_icon.dart';
|
||||
import 'package:spotiflac_android/widgets/app_loading_indicator.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
@@ -822,62 +823,38 @@ class _SearchProviderSelector extends ConsumerWidget {
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
ctx.l10n.extensionsSearchProvider,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
title: context.l10n.extensionsSearchProvider,
|
||||
subtitle: context.l10n.extensionsSearchProviderDescription,
|
||||
builder: (ctx, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...searchProviders.map(
|
||||
(ext) => AppSheetOption(
|
||||
leading: AppIcon(Icons.extension, color: colorScheme.secondary),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(
|
||||
ext.searchBehavior?.placeholder ??
|
||||
ctx.l10n.extensionsCustomSearch,
|
||||
),
|
||||
trailing: settings.searchProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
Icons.circle_outlined,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(ext.id);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
ctx.l10n.extensionsSearchProviderDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
...searchProviders.map(
|
||||
(ext) => ListTile(
|
||||
leading: AppIcon(
|
||||
Icons.extension,
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(
|
||||
ext.searchBehavior?.placeholder ??
|
||||
ctx.l10n.extensionsCustomSearch,
|
||||
),
|
||||
trailing: settings.searchProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
Icons.circle_outlined,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSearchProvider(ext.id);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -992,61 +969,62 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
ctx.l10n.extensionsHomeFeedProvider,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
ctx.l10n.extensionsHomeFeedDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: context.l10n.extensionsHomeFeedProvider,
|
||||
subtitle: context.l10n.extensionsHomeFeedDescription,
|
||||
builder: (ctx, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppSheetOption(
|
||||
leading: AppIcon(Icons.auto_awesome, color: colorScheme.primary),
|
||||
title: Text(ctx.l10n.extensionsHomeFeedAuto),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedAutoSubtitle),
|
||||
trailing:
|
||||
(settings.homeFeedProvider == null ||
|
||||
settings.homeFeedProvider!.isEmpty)
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setHomeFeedProvider(null);
|
||||
ref.read(exploreProvider.notifier).refresh();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: AppIcon(Icons.block, color: colorScheme.error),
|
||||
title: Text(ctx.l10n.extensionsHomeFeedOff),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedOffSubtitle),
|
||||
trailing:
|
||||
settings.homeFeedProvider == AppSettings.homeFeedProviderOff
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setHomeFeedProvider(AppSettings.homeFeedProviderOff);
|
||||
ref.read(exploreProvider.notifier).clear();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
if (homeFeedProviders.isEmpty)
|
||||
AppSheetOption(
|
||||
enabled: false,
|
||||
leading: AppIcon(
|
||||
Icons.auto_awesome,
|
||||
color: colorScheme.primary,
|
||||
Icons.extension_off,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
title: Text(ctx.l10n.extensionsHomeFeedAuto),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedAutoSubtitle),
|
||||
trailing:
|
||||
(settings.homeFeedProvider == null ||
|
||||
settings.homeFeedProvider!.isEmpty)
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
Icons.circle_outlined,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
onTap: () {
|
||||
ref.read(settingsProvider.notifier).setHomeFeedProvider(null);
|
||||
ref.read(exploreProvider.notifier).refresh();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
title: Text(ctx.l10n.extensionsNoHomeFeedExtensions),
|
||||
),
|
||||
ListTile(
|
||||
leading: AppIcon(Icons.block, color: colorScheme.error),
|
||||
title: Text(ctx.l10n.extensionsHomeFeedOff),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedOffSubtitle),
|
||||
trailing:
|
||||
settings.homeFeedProvider == AppSettings.homeFeedProviderOff
|
||||
...homeFeedProviders.map(
|
||||
(ext) => AppSheetOption(
|
||||
leading: AppIcon(Icons.extension, color: colorScheme.secondary),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(ctx.l10n.extensionsHomeFeedUse(ext.displayName)),
|
||||
trailing: settings.homeFeedProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
Icons.circle_outlined,
|
||||
@@ -1055,48 +1033,14 @@ class _HomeFeedProviderSelector extends ConsumerWidget {
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setHomeFeedProvider(AppSettings.homeFeedProviderOff);
|
||||
ref.read(exploreProvider.notifier).clear();
|
||||
.setHomeFeedProvider(ext.id);
|
||||
ref.read(exploreProvider.notifier).refresh();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
if (homeFeedProviders.isEmpty)
|
||||
ListTile(
|
||||
enabled: false,
|
||||
leading: AppIcon(
|
||||
Icons.extension_off,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
title: Text(ctx.l10n.extensionsNoHomeFeedExtensions),
|
||||
),
|
||||
...homeFeedProviders.map(
|
||||
(ext) => ListTile(
|
||||
leading: AppIcon(
|
||||
Icons.extension,
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(
|
||||
ctx.l10n.extensionsHomeFeedUse(ext.displayName),
|
||||
),
|
||||
trailing: settings.homeFeedProvider == ext.id
|
||||
? AppIcon(Icons.check_circle, color: colorScheme.primary)
|
||||
: AppIcon(
|
||||
Icons.circle_outlined,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setHomeFeedProvider(ext.id);
|
||||
ref.read(exploreProvider.notifier).refresh();
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -519,33 +519,16 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final isSafMode =
|
||||
settings.storageMode == 'saf' && settings.downloadTreeUri.isNotEmpty;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (ctx) => SafeArea(
|
||||
title: context.l10n.setupDownloadLocationTitle,
|
||||
subtitle: context.l10n.downloadLocationSubtitle,
|
||||
builder: (ctx, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.setupDownloadLocationTitle,
|
||||
style: Theme.of(
|
||||
ctx,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.downloadLocationSubtitle,
|
||||
style: Theme.of(ctx).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
Icons.folder_special,
|
||||
@@ -583,33 +566,16 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
|
||||
void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (ctx) => SafeArea(
|
||||
title: context.l10n.setupDownloadLocationTitle,
|
||||
subtitle: context.l10n.setupDownloadLocationIosMessage,
|
||||
builder: (ctx, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.setupDownloadLocationTitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.setupDownloadLocationIosMessage,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
Icons.folder_special,
|
||||
@@ -708,20 +674,27 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
String? title,
|
||||
String? description,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final save =
|
||||
onSave ?? ref.read(settingsProvider.notifier).setFilenameFormat;
|
||||
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: colorScheme.surface,
|
||||
builder: (context) => _FilenameFormatEditorSheet(
|
||||
title: title ?? context.l10n.filenameFormat,
|
||||
subtitle:
|
||||
description ??
|
||||
context.l10n.downloadFilenameDescription(
|
||||
'{album}',
|
||||
'{artist}',
|
||||
'{date}',
|
||||
'{disc}',
|
||||
'{title}',
|
||||
'{track}',
|
||||
'{year}',
|
||||
),
|
||||
builder: (context, controller) => _FilenameFormatEditorSheet(
|
||||
initialText: current,
|
||||
onSave: save,
|
||||
title: title,
|
||||
description: description,
|
||||
scrollController: controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -731,93 +704,69 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.7,
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.downloadAlbumFolderStructure,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
title: context.l10n.downloadAlbumFolderStructure,
|
||||
subtitle: context.l10n.albumFolderStructureDescription,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final option in [
|
||||
(
|
||||
'artist_album',
|
||||
context.l10n.albumFolderArtistAlbum,
|
||||
context.l10n.albumFolderArtistAlbumSubtitle,
|
||||
Icons.folder_outlined,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.albumFolderStructureDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
(
|
||||
'artist_year_album',
|
||||
context.l10n.albumFolderArtistYearAlbum,
|
||||
context.l10n.albumFolderArtistYearAlbumSubtitle,
|
||||
Icons.calendar_today_outlined,
|
||||
),
|
||||
for (final option in [
|
||||
(
|
||||
'artist_album',
|
||||
context.l10n.albumFolderArtistAlbum,
|
||||
context.l10n.albumFolderArtistAlbumSubtitle,
|
||||
Icons.folder_outlined,
|
||||
),
|
||||
(
|
||||
'artist_year_album',
|
||||
context.l10n.albumFolderArtistYearAlbum,
|
||||
context.l10n.albumFolderArtistYearAlbumSubtitle,
|
||||
Icons.calendar_today_outlined,
|
||||
),
|
||||
(
|
||||
'album_only',
|
||||
context.l10n.albumFolderAlbumOnly,
|
||||
context.l10n.albumFolderAlbumOnlySubtitle,
|
||||
Icons.album_outlined,
|
||||
),
|
||||
(
|
||||
'year_album',
|
||||
context.l10n.albumFolderYearAlbum,
|
||||
context.l10n.albumFolderYearAlbumSubtitle,
|
||||
Icons.event_outlined,
|
||||
),
|
||||
(
|
||||
'artist_album_singles',
|
||||
context.l10n.albumFolderArtistAlbumSingles,
|
||||
context.l10n.albumFolderArtistAlbumSinglesSubtitle,
|
||||
Icons.person_outlined,
|
||||
),
|
||||
(
|
||||
'artist_album_flat',
|
||||
context.l10n.albumFolderArtistAlbumFlat,
|
||||
context.l10n.albumFolderArtistAlbumFlatSubtitle,
|
||||
Icons.person_outline_outlined,
|
||||
),
|
||||
])
|
||||
AppSheetOption(
|
||||
leading: AppIcon(option.$4),
|
||||
title: Text(option.$2),
|
||||
subtitle: Text(option.$3),
|
||||
trailing: current == option.$1
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAlbumFolderStructure(option.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
'album_only',
|
||||
context.l10n.albumFolderAlbumOnly,
|
||||
context.l10n.albumFolderAlbumOnlySubtitle,
|
||||
Icons.album_outlined,
|
||||
),
|
||||
(
|
||||
'year_album',
|
||||
context.l10n.albumFolderYearAlbum,
|
||||
context.l10n.albumFolderYearAlbumSubtitle,
|
||||
Icons.event_outlined,
|
||||
),
|
||||
(
|
||||
'artist_album_singles',
|
||||
context.l10n.albumFolderArtistAlbumSingles,
|
||||
context.l10n.albumFolderArtistAlbumSinglesSubtitle,
|
||||
Icons.person_outlined,
|
||||
),
|
||||
(
|
||||
'artist_album_flat',
|
||||
context.l10n.albumFolderArtistAlbumFlat,
|
||||
context.l10n.albumFolderArtistAlbumFlatSubtitle,
|
||||
Icons.person_outline_outlined,
|
||||
),
|
||||
])
|
||||
AppSheetOption(
|
||||
leading: AppIcon(option.$4),
|
||||
title: Text(option.$2),
|
||||
subtitle: Text(option.$3),
|
||||
trailing: current == option.$1
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAlbumFolderStructure(option.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -828,85 +777,61 @@ class _FilesSettingsPageState extends ConsumerState<FilesSettingsPage> {
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.7,
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.downloadFolderOrganization,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
title: context.l10n.downloadFolderOrganization,
|
||||
subtitle: context.l10n.folderOrganizationDescription,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final option in [
|
||||
(
|
||||
'none',
|
||||
context.l10n.folderOrganizationNone,
|
||||
context.l10n.folderOrganizationNoneSubtitle,
|
||||
'SpotiFLAC/Track.flac',
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.folderOrganizationDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
(
|
||||
'playlist',
|
||||
context.l10n.folderOrganizationByPlaylist,
|
||||
context.l10n.folderOrganizationByPlaylistSubtitle,
|
||||
'SpotiFLAC/Playlist Name/Track.flac',
|
||||
),
|
||||
for (final option in [
|
||||
(
|
||||
'none',
|
||||
context.l10n.folderOrganizationNone,
|
||||
context.l10n.folderOrganizationNoneSubtitle,
|
||||
'SpotiFLAC/Track.flac',
|
||||
),
|
||||
(
|
||||
'playlist',
|
||||
context.l10n.folderOrganizationByPlaylist,
|
||||
context.l10n.folderOrganizationByPlaylistSubtitle,
|
||||
'SpotiFLAC/Playlist Name/Track.flac',
|
||||
),
|
||||
(
|
||||
'artist',
|
||||
context.l10n.folderOrganizationByArtist,
|
||||
context.l10n.folderOrganizationByArtistSubtitle,
|
||||
'SpotiFLAC/Artist Name/Track.flac',
|
||||
),
|
||||
(
|
||||
'album',
|
||||
context.l10n.folderOrganizationByAlbum,
|
||||
context.l10n.folderOrganizationByAlbumSubtitle,
|
||||
'SpotiFLAC/Album Name/Track.flac',
|
||||
),
|
||||
(
|
||||
'artist_album',
|
||||
context.l10n.folderOrganizationByArtistAlbum,
|
||||
context.l10n.folderOrganizationByArtistAlbumSubtitle,
|
||||
'SpotiFLAC/Artist/Album/Track.flac',
|
||||
),
|
||||
])
|
||||
_FolderOption(
|
||||
title: option.$2,
|
||||
subtitle: option.$3,
|
||||
example: option.$4,
|
||||
isSelected: current == option.$1,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setFolderOrganization(option.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
'artist',
|
||||
context.l10n.folderOrganizationByArtist,
|
||||
context.l10n.folderOrganizationByArtistSubtitle,
|
||||
'SpotiFLAC/Artist Name/Track.flac',
|
||||
),
|
||||
(
|
||||
'album',
|
||||
context.l10n.folderOrganizationByAlbum,
|
||||
context.l10n.folderOrganizationByAlbumSubtitle,
|
||||
'SpotiFLAC/Album Name/Track.flac',
|
||||
),
|
||||
(
|
||||
'artist_album',
|
||||
context.l10n.folderOrganizationByArtistAlbum,
|
||||
context.l10n.folderOrganizationByArtistAlbumSubtitle,
|
||||
'SpotiFLAC/Artist/Album/Track.flac',
|
||||
),
|
||||
])
|
||||
_FolderOption(
|
||||
title: option.$2,
|
||||
subtitle: option.$3,
|
||||
example: option.$4,
|
||||
isSelected: current == option.$1,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setFolderOrganization(option.$1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -961,14 +886,12 @@ class _FolderOption extends StatelessWidget {
|
||||
class _FilenameFormatEditorSheet extends StatefulWidget {
|
||||
final String initialText;
|
||||
final void Function(String) onSave;
|
||||
final String? title;
|
||||
final String? description;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const _FilenameFormatEditorSheet({
|
||||
required this.initialText,
|
||||
required this.onSave,
|
||||
this.title,
|
||||
this.description,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -1069,133 +992,98 @@ class _FilenameFormatEditorSheetState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
|
||||
child: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const AppSheetHandle(),
|
||||
Text(
|
||||
widget.title ?? context.l10n.filenameFormat,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.description ??
|
||||
context.l10n.downloadFilenameDescription(
|
||||
'{album}',
|
||||
'{artist}',
|
||||
'{date}',
|
||||
'{disc}',
|
||||
'{title}',
|
||||
'{track}',
|
||||
'{year}',
|
||||
),
|
||||
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(
|
||||
context.l10n.downloadFilenameInsertTag,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _basicTags
|
||||
.map((tag) => _tagChip(colorScheme, tag))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppSwitchListTile(
|
||||
value: _showAdvancedTags,
|
||||
onChanged: (value) =>
|
||||
setState(() => _showAdvancedTags = value),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.filenameShowAdvancedTags),
|
||||
subtitle: Text(
|
||||
context.l10n.filenameShowAdvancedTagsDescription,
|
||||
),
|
||||
),
|
||||
if (_showAdvancedTags) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _advancedTags
|
||||
.map((tag) => _tagChip(colorScheme, tag))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppDialogAction(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AppDialogAction(
|
||||
filled: true,
|
||||
onPressed: () {
|
||||
widget.onSave(_controller.text);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
controller: widget.scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
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(
|
||||
context.l10n.downloadFilenameInsertTag,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _basicTags
|
||||
.map((tag) => _tagChip(colorScheme, tag))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AppSwitchListTile(
|
||||
value: _showAdvancedTags,
|
||||
onChanged: (value) => setState(() => _showAdvancedTags = value),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.filenameShowAdvancedTags),
|
||||
subtitle: Text(context.l10n.filenameShowAdvancedTagsDescription),
|
||||
),
|
||||
if (_showAdvancedTags) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _advancedTags
|
||||
.map((tag) => _tagChip(colorScheme, tag))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppDialogAction(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AppDialogAction(
|
||||
filled: true,
|
||||
onPressed: () {
|
||||
widget.onSave(_controller.text);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,33 +336,16 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
|
||||
void _showAutoScanPicker(BuildContext context, String current) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.libraryAutoScan,
|
||||
subtitle: context.l10n.libraryAutoScanSubtitle,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.libraryAutoScan,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.libraryAutoScanSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
_AutoScanOption(
|
||||
icon: Icons.block,
|
||||
title: context.l10n.libraryAutoScanOff,
|
||||
@@ -445,24 +428,15 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
('artists', Icons.person, context.l10n.searchArtists),
|
||||
('playlists', Icons.queue_music, context.l10n.searchPlaylists),
|
||||
];
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.libraryDefaultView,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.libraryDefaultView,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
for (final (value, icon, label) in options)
|
||||
_AutoScanOption(
|
||||
icon: icon,
|
||||
@@ -532,24 +506,15 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
context.l10n.libraryQualityLabelFileFormat,
|
||||
),
|
||||
];
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.trackAudioQuality,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.trackAudioQuality,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
for (final (value, icon, label) in options)
|
||||
_AutoScanOption(
|
||||
icon: icon,
|
||||
|
||||
@@ -229,34 +229,16 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
title: context.l10n.lyricsMode,
|
||||
subtitle: context.l10n.lyricsModeDescription,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.lyricsMode,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.lyricsModeDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.audiotrack),
|
||||
title: Text(context.l10n.lyricsModeEmbed),
|
||||
@@ -301,39 +283,19 @@ class LyricsSettingsPage extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
String currentLanguage,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final controller = TextEditingController(text: currentLanguage);
|
||||
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 24,
|
||||
bottom: 24 + MediaQuery.viewInsetsOf(context).bottom,
|
||||
),
|
||||
title: context.l10n.downloadMusixmatchLanguage,
|
||||
subtitle: context.l10n.downloadMusixmatchLanguageDesc,
|
||||
builder: (context, scrollController) => SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.downloadMusixmatchLanguage,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.downloadMusixmatchLanguageDesc,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
textInputAction: TextInputAction.done,
|
||||
|
||||
@@ -186,59 +186,36 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
int currentMaxDimension,
|
||||
) {
|
||||
const options = [0, 500, 1000, 1500, 2000];
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.optionsEmbeddedCoverSize,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
title: context.l10n.optionsEmbeddedCoverSize,
|
||||
subtitle: context.l10n.optionsEmbeddedCoverSizeDescription,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final maxDimension in options)
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
maxDimension == 0
|
||||
? Icons.image_outlined
|
||||
: Icons.compress_outlined,
|
||||
),
|
||||
title: Text(_getEmbeddedCoverSizeLabel(context, maxDimension)),
|
||||
trailing: currentMaxDimension == maxDimension
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setEmbeddedCoverMaxDimension(maxDimension);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
|
||||
child: Text(
|
||||
context.l10n.optionsEmbeddedCoverSizeDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final maxDimension in options)
|
||||
AppSheetOption(
|
||||
leading: AppIcon(
|
||||
maxDimension == 0
|
||||
? Icons.image_outlined
|
||||
: Icons.compress_outlined,
|
||||
),
|
||||
title: Text(
|
||||
_getEmbeddedCoverSizeLabel(context, maxDimension),
|
||||
),
|
||||
trailing: currentMaxDimension == maxDimension
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setEmbeddedCoverMaxDimension(maxDimension);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -249,87 +226,62 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
WidgetRef ref,
|
||||
String currentMode,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showAppModalBottomSheet<void>(
|
||||
showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
// Three described options can exceed the default sheet height on
|
||||
// small or landscape screens, as the cover-size picker above does.
|
||||
isScrollControlled: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.optionsArtistTagMode,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
title: context.l10n.optionsArtistTagMode,
|
||||
subtitle: context.l10n.optionsArtistTagModeDescription,
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.segment_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeJoined),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModeJoinedSubtitle),
|
||||
trailing: currentMode == artistTagModeJoined
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModeJoined);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.library_music_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeSplitVorbis),
|
||||
subtitle: Text(
|
||||
context.l10n.optionsArtistTagModeSplitVorbisSubtitle,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.optionsArtistTagModeDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.segment_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeJoined),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModeJoinedSubtitle),
|
||||
trailing: currentMode == artistTagModeJoined
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModeJoined);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.library_music_outlined),
|
||||
title: Text(context.l10n.optionsArtistTagModeSplitVorbis),
|
||||
subtitle: Text(
|
||||
context.l10n.optionsArtistTagModeSplitVorbisSubtitle,
|
||||
),
|
||||
trailing: currentMode == artistTagModeSplitVorbis
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModeSplitVorbis);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.person_outline),
|
||||
title: Text(context.l10n.optionsArtistTagModePrimary),
|
||||
subtitle: Text(
|
||||
context.l10n.optionsArtistTagModePrimarySubtitle,
|
||||
),
|
||||
trailing: currentMode == artistTagModePrimary
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModePrimary);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
trailing: currentMode == artistTagModeSplitVorbis
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModeSplitVorbis);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
AppSheetOption(
|
||||
leading: const AppIcon(Icons.person_outline),
|
||||
title: Text(context.l10n.optionsArtistTagModePrimary),
|
||||
subtitle: Text(context.l10n.optionsArtistTagModePrimarySubtitle),
|
||||
trailing: currentMode == artistTagModePrimary
|
||||
? const AppIcon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setArtistTagMode(artistTagModePrimary);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ class AppBottomSheet extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
subtitle!,
|
||||
maxLines: context.isMornye ? 3 : 1,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
@@ -184,6 +184,130 @@ Future<T?> showAppBottomSheet<T>({
|
||||
);
|
||||
}
|
||||
|
||||
/// Fits short content and coordinates scrolling/dragging for longer sheets.
|
||||
class AppScrollableSheet extends StatefulWidget {
|
||||
const AppScrollableSheet({super.key, required this.builder});
|
||||
|
||||
final ScrollableWidgetBuilder builder;
|
||||
|
||||
@override
|
||||
State<AppScrollableSheet> createState() => _AppScrollableSheetState();
|
||||
}
|
||||
|
||||
class _AppScrollableSheetState extends State<AppScrollableSheet> {
|
||||
final _contentKey = GlobalKey();
|
||||
final _sheetController = DraggableScrollableController();
|
||||
ScrollController? _scrollController;
|
||||
double _availableHeight = 0;
|
||||
double _initialSize = 0.85;
|
||||
double _maxSize = 0.95;
|
||||
bool _fitScheduled = false;
|
||||
|
||||
void _scheduleFit() {
|
||||
if (_fitScheduled) return;
|
||||
_fitScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_fitScheduled = false;
|
||||
if (!mounted || _availableHeight <= 0) return;
|
||||
final box = _contentKey.currentContext?.findRenderObject();
|
||||
final scroll = _scrollController;
|
||||
if (box is! RenderBox || !box.hasSize || scroll == null) return;
|
||||
final overflow = scroll.hasClients && scroll.position.hasContentDimensions
|
||||
? (scroll.position.maxScrollExtent - scroll.position.minScrollExtent)
|
||||
.clamp(0.0, double.infinity)
|
||||
: 0.0;
|
||||
// Align lets the header/body shrink below the allocated sheet height.
|
||||
// Adding the scroll overflow recovers the full content height without
|
||||
// an intrinsic-layout pass or a second hidden copy of the form.
|
||||
final contentFraction = (box.size.height + overflow) / _availableHeight;
|
||||
final initial = contentFraction.clamp(0.01, 0.85);
|
||||
final maximum = contentFraction.clamp(initial, 0.95);
|
||||
if ((initial - _initialSize).abs() * _availableHeight < 1 &&
|
||||
(maximum - _maxSize).abs() * _availableHeight < 1) {
|
||||
return;
|
||||
}
|
||||
final resize = (initial - _initialSize).abs() * _availableHeight >= 1;
|
||||
setState(() {
|
||||
_initialSize = initial;
|
||||
_maxSize = maximum;
|
||||
});
|
||||
// A field or search result can change after the user has dragged. The
|
||||
// framework otherwise preserves that old extent when content grows.
|
||||
if (resize) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _sheetController.isAttached) {
|
||||
_sheetController.jumpTo(_initialSize);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_availableHeight = constraints.maxHeight;
|
||||
_scheduleFit();
|
||||
return NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.depth == 0) _scheduleFit();
|
||||
return false;
|
||||
},
|
||||
child: NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (_) {
|
||||
_scheduleFit();
|
||||
return false;
|
||||
},
|
||||
child: DraggableScrollableSheet(
|
||||
controller: _sheetController,
|
||||
expand: false,
|
||||
initialChildSize: _initialSize,
|
||||
minChildSize: (_initialSize * 0.6).clamp(0.0, 0.5),
|
||||
maxChildSize: _maxSize,
|
||||
snap: true,
|
||||
snapSizes: [_initialSize],
|
||||
builder: (context, controller) {
|
||||
_scrollController = controller;
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizeChangedLayoutNotifier(
|
||||
key: _contentKey,
|
||||
child: widget.builder(context, controller),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A fixed handle/header above a body that scrolls and drags the same sheet.
|
||||
Future<T?> showAppScrollableBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
required ScrollableWidgetBuilder builder,
|
||||
required String title,
|
||||
String? subtitle,
|
||||
}) => showAppModalBottomSheet<T>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => AppScrollableSheet(
|
||||
builder: (context, controller) => AppBottomSheet(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
child: builder(context, controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Adaptive surface for sheets which already supply their own header/layout.
|
||||
Future<T?> showAppModalBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
|
||||
@@ -30,6 +30,167 @@ void main() {
|
||||
: ThemeData(brightness: brightness);
|
||||
final variant = 'Mornye: $mornye, $brightness';
|
||||
|
||||
testWidgets(
|
||||
'short sheets fit their content and resize with options ($variant)',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final count = ValueNotifier(2);
|
||||
addTearDown(count.dispose);
|
||||
await tester.pumpWidget(
|
||||
_app(
|
||||
theme,
|
||||
Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
child: const Text('Open options'),
|
||||
onPressed: () => showAppScrollableBottomSheet<void>(
|
||||
context: context,
|
||||
title: 'Options',
|
||||
builder: (context, controller) =>
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: count,
|
||||
builder: (context, count, _) =>
|
||||
SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < count; i++)
|
||||
AppSheetOption(
|
||||
title: Text('Option $i'),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Open options'));
|
||||
await tester.pumpAndSettle();
|
||||
final sheet = find.byType(AppBottomSheet);
|
||||
final shortHeight = tester.getSize(sheet).height;
|
||||
expect(shortHeight, lessThan(844 * 0.5));
|
||||
expect(
|
||||
tester.getRect(sheet).bottom -
|
||||
tester.getRect(find.text('Option 1')).bottom,
|
||||
lessThan(40),
|
||||
);
|
||||
final scroll = find.descendant(
|
||||
of: sheet,
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
);
|
||||
await tester.drag(scroll, const Offset(0, -250));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(sheet).height, closeTo(shortHeight, 1));
|
||||
count.value = 4;
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(sheet).height, greaterThan(shortHeight));
|
||||
expect(tester.getSize(sheet).height, lessThan(844 * 0.85));
|
||||
count.value = 40;
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(sheet).height, closeTo(844 * 0.85, 1));
|
||||
await tester.drag(scroll, const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
count.value = 1;
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getSize(sheet).height, lessThan(shortHeight));
|
||||
expect(find.text('Option 0').hitTestable(), findsOneWidget);
|
||||
await tester.fling(scroll, const Offset(0, 200), 1200);
|
||||
await tester.pumpAndSettle();
|
||||
expect(sheet, findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'scrollable sheet keeps its header and dismisses from body or handle ($variant)',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
int? selected;
|
||||
await tester.pumpWidget(
|
||||
_app(
|
||||
theme,
|
||||
Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
child: const Text('Open options'),
|
||||
onPressed: () async {
|
||||
selected = await showAppScrollableBottomSheet<int>(
|
||||
context: context,
|
||||
title: 'Options',
|
||||
builder: (context, controller) => SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < 40; i++)
|
||||
AppSheetOption(
|
||||
title: Text('Option $i'),
|
||||
onTap: () => Navigator.pop(context, i),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
Future<void> open() async {
|
||||
await tester.tap(find.text('Open options'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSize(find.byType(AppBottomSheet)).height,
|
||||
closeTo(844 * 0.85, 1),
|
||||
);
|
||||
expect(find.byType(AppSheetHandle).hitTestable(), findsOneWidget);
|
||||
}
|
||||
|
||||
await open();
|
||||
final scroll = find.descendant(
|
||||
of: find.byType(AppBottomSheet),
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
);
|
||||
await tester.drag(scroll, const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getSize(find.byType(AppBottomSheet)).height,
|
||||
closeTo(844 * 0.95, 1),
|
||||
);
|
||||
expect(find.text('Options').hitTestable(), findsOneWidget);
|
||||
expect(find.byType(AppSheetHandle).hitTestable(), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('Option 39'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Option 39'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(selected, 39);
|
||||
await open();
|
||||
await tester.fling(scroll, const Offset(0, 400), 1200);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AppBottomSheet), findsNothing);
|
||||
await open();
|
||||
await tester.drag(scroll, const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.fling(
|
||||
find.byType(AppSheetHandle),
|
||||
const Offset(0, 300),
|
||||
1200,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AppBottomSheet), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('sheet selects, disables and returns values ($variant)', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -343,7 +343,8 @@ void main() {
|
||||
expect(find.text('Shell navigation').hitTestable(), findsNothing);
|
||||
final sortSheet = find.byType(AppBottomSheet);
|
||||
final sheetHeight = tester.getSize(sortSheet).height;
|
||||
expect(sheetHeight, lessThan(915 * 0.75));
|
||||
expect(sheetHeight, lessThan(915 * 0.85));
|
||||
expect(find.text('Genre (Z-A)').hitTestable(), findsOneWidget);
|
||||
final sheetScroll = find.descendant(
|
||||
of: sortSheet,
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
@@ -365,7 +366,16 @@ void main() {
|
||||
await tester.tap(find.text('Filters').hitTestable());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(tester.getSize(sortSheet).height, closeTo(sheetHeight, 1));
|
||||
expect(tester.getSize(sortSheet).height, closeTo(915 * 0.85, 1));
|
||||
expect(tester.getSize(sortSheet).height, greaterThan(sheetHeight));
|
||||
await tester.drag(sheetScroll, const Offset(0, -200));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(tester.getSize(sortSheet).height, closeTo(915 * 0.95, 1));
|
||||
// At full height, return the body to its top before swiping to dismiss.
|
||||
await tester.drag(sheetScroll, const Offset(0, 200));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.drag(sheetScroll, const Offset(0, 450));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
@@ -378,7 +388,11 @@ void main() {
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(find.byType(AppSheetHandle).hitTestable(), findsOneWidget);
|
||||
expect(find.text('Reset').hitTestable(), findsOneWidget);
|
||||
await tester.drag(find.byType(AppSheetHandle), const Offset(0, 450));
|
||||
await tester.fling(
|
||||
find.byType(AppSheetHandle),
|
||||
const Offset(0, 300),
|
||||
1000,
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(sortSheet, findsNothing);
|
||||
|
||||
Reference in new issue
Block a user