mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-25 13:30:55 +02:00
4d6f7d8b08
Add localization keys for verification browser settings, manual verification help dialog actions, and lossless conversion dithering and resampler option labels.
517 lines
17 KiB
Dart
517 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
|
import 'package:spotiflac_android/providers/settings_provider.dart';
|
|
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
|
import 'package:spotiflac_android/widgets/settings_group.dart';
|
|
|
|
class AppSettingsPage extends ConsumerWidget {
|
|
const AppSettingsPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final settings = ref.watch(settingsProvider);
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final topPadding = normalizedHeaderTopPadding(context);
|
|
|
|
return PopScope(
|
|
canPop: true,
|
|
child: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
SliverAppBar(
|
|
expandedHeight: 120 + topPadding,
|
|
collapsedHeight: kToolbarHeight,
|
|
floating: false,
|
|
pinned: true,
|
|
backgroundColor: colorScheme.surface,
|
|
surfaceTintColor: Colors.transparent,
|
|
leading: IconButton(
|
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
flexibleSpace: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final maxHeight = 120 + topPadding;
|
|
final minHeight = kToolbarHeight + topPadding;
|
|
final expandRatio =
|
|
((constraints.maxHeight - minHeight) /
|
|
(maxHeight - minHeight))
|
|
.clamp(0.0, 1.0);
|
|
final leftPadding = 56 - (32 * expandRatio);
|
|
return FlexibleSpaceBar(
|
|
expandedTitleScale: 1.0,
|
|
titlePadding: EdgeInsets.only(
|
|
left: leftPadding,
|
|
bottom: 16,
|
|
),
|
|
title: Text(
|
|
context.l10n.settingsApp,
|
|
style: TextStyle(
|
|
fontSize: 20 + (8 * expandRatio),
|
|
fontWeight: FontWeight.bold,
|
|
color: colorScheme.onSurface,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionApp),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
SettingsSwitchItem(
|
|
icon: Icons.extension,
|
|
title: context.l10n.optionsExtensionStore,
|
|
subtitle: context.l10n.optionsExtensionStoreSubtitle,
|
|
value: settings.showExtensionStore,
|
|
onChanged: (v) => ref
|
|
.read(settingsProvider.notifier)
|
|
.setShowExtensionStore(v),
|
|
),
|
|
_VerificationBrowserModeSelector(
|
|
currentMode: settings.extensionVerificationBrowserMode,
|
|
onChanged: (mode) => ref
|
|
.read(settingsProvider.notifier)
|
|
.setExtensionVerificationBrowserMode(mode),
|
|
),
|
|
SettingsSwitchItem(
|
|
icon: Icons.system_update,
|
|
title: context.l10n.optionsCheckUpdates,
|
|
subtitle: context.l10n.optionsCheckUpdatesSubtitle,
|
|
value: settings.checkForUpdates,
|
|
onChanged: (v) => ref
|
|
.read(settingsProvider.notifier)
|
|
.setCheckForUpdates(v),
|
|
showDivider: settings.checkForUpdates,
|
|
),
|
|
if (settings.checkForUpdates)
|
|
_UpdateChannelSelector(
|
|
currentChannel: settings.updateChannel,
|
|
onChanged: (v) => ref
|
|
.read(settingsProvider.notifier)
|
|
.setUpdateChannel(v),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionData),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
SettingsItem(
|
|
icon: Icons.cleaning_services_outlined,
|
|
title: context.l10n.cleanupOrphanedDownloads,
|
|
subtitle: context.l10n.cleanupOrphanedDownloadsSubtitle,
|
|
onTap: () => _cleanupOrphanedDownloads(context, ref),
|
|
),
|
|
SettingsItem(
|
|
icon: Icons.delete_forever,
|
|
title: context.l10n.optionsClearHistory,
|
|
subtitle: context.l10n.optionsClearHistorySubtitle,
|
|
onTap: () =>
|
|
_showClearHistoryDialog(context, ref, colorScheme),
|
|
),
|
|
SettingsSwitchItem(
|
|
icon: Icons.history_toggle_off_outlined,
|
|
title: context.l10n.settingsSaveDownloadHistory,
|
|
subtitle: context.l10n.settingsSaveDownloadHistorySubtitle,
|
|
value: settings.saveDownloadHistory,
|
|
onChanged: (enabled) {
|
|
if (enabled) {
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setSaveDownloadHistory(true);
|
|
return;
|
|
}
|
|
|
|
final hasHistory = ref
|
|
.read(downloadHistoryProvider)
|
|
.items
|
|
.isNotEmpty;
|
|
if (hasHistory) {
|
|
_showDisableHistoryDialog(context, ref, colorScheme);
|
|
return;
|
|
}
|
|
|
|
ref
|
|
.read(settingsProvider.notifier)
|
|
.setSaveDownloadHistory(false);
|
|
},
|
|
showDivider: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SliverToBoxAdapter(
|
|
child: SettingsSectionHeader(title: context.l10n.sectionDebug),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SettingsGroup(
|
|
children: [
|
|
SettingsSwitchItem(
|
|
icon: Icons.bug_report,
|
|
title: context.l10n.optionsDetailedLogging,
|
|
subtitle: settings.enableLogging
|
|
? context.l10n.optionsDetailedLoggingOn
|
|
: context.l10n.optionsDetailedLoggingOff,
|
|
value: settings.enableLogging,
|
|
onChanged: (v) =>
|
|
ref.read(settingsProvider.notifier).setEnableLogging(v),
|
|
showDivider: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showDisableHistoryDialog(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
ColorScheme colorScheme,
|
|
) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(context.l10n.dialogDisableHistoryTitle),
|
|
content: Text(context.l10n.dialogDisableHistoryMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(context.l10n.dialogCancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
ref.read(downloadHistoryProvider.notifier).clearHistory();
|
|
ref.read(settingsProvider.notifier).setSaveDownloadHistory(false);
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.snackbarHistoryCleared)),
|
|
);
|
|
},
|
|
child: Text(
|
|
context.l10n.dialogDisableAndClear,
|
|
style: TextStyle(color: colorScheme.error),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showClearHistoryDialog(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
ColorScheme colorScheme,
|
|
) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(context.l10n.dialogClearHistoryTitle),
|
|
content: Text(context.l10n.dialogClearHistoryMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(context.l10n.dialogCancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
ref.read(downloadHistoryProvider.notifier).clearHistory();
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.snackbarHistoryCleared)),
|
|
);
|
|
},
|
|
child: Text(
|
|
context.l10n.dialogClear,
|
|
style: TextStyle(color: colorScheme.error),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _cleanupOrphanedDownloads(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
) async {
|
|
showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
content: Row(
|
|
children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(width: 16),
|
|
Text(context.l10n.cleanupOrphanedDownloads),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
try {
|
|
final removed = await ref
|
|
.read(downloadHistoryProvider.notifier)
|
|
.cleanupOrphanedDownloads();
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
removed > 0
|
|
? context.l10n.cleanupOrphanedDownloadsResult(removed)
|
|
: context.l10n.cleanupOrphanedDownloadsNone,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.snackbarError(e.toString()))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class _UpdateChannelSelector extends StatelessWidget {
|
|
final String currentChannel;
|
|
final ValueChanged<String> onChanged;
|
|
const _UpdateChannelSelector({
|
|
required this.currentChannel,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.new_releases,
|
|
color: colorScheme.onSurfaceVariant,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
context.l10n.optionsUpdateChannel,
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
currentChannel == 'preview'
|
|
? context.l10n.optionsUpdateChannelPreview
|
|
: context.l10n.optionsUpdateChannelStable,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
_ChannelChip(
|
|
label: context.l10n.channelStable,
|
|
isSelected: currentChannel == 'stable',
|
|
onTap: () => onChanged('stable'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ChannelChip(
|
|
label: context.l10n.channelPreview,
|
|
isSelected: currentChannel == 'preview',
|
|
onTap: () => onChanged('preview'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.info_outline,
|
|
size: 16,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
context.l10n.optionsUpdateChannelWarning,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _VerificationBrowserModeSelector extends StatelessWidget {
|
|
final String currentMode;
|
|
final ValueChanged<String> onChanged;
|
|
|
|
const _VerificationBrowserModeSelector({
|
|
required this.currentMode,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final normalizedMode = currentMode == 'in_app_first'
|
|
? 'in_app_first'
|
|
: 'external_first';
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.open_in_browser,
|
|
color: colorScheme.onSurfaceVariant,
|
|
size: 24,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
context.l10n.extensionVerificationBrowserTitle,
|
|
style: Theme.of(context).textTheme.bodyLarge,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
normalizedMode == 'external_first'
|
|
? context
|
|
.l10n
|
|
.extensionVerificationBrowserSubtitleExternal
|
|
: context
|
|
.l10n
|
|
.extensionVerificationBrowserSubtitleInApp,
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
?.copyWith(color: colorScheme.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
_ChannelChip(
|
|
label: context.l10n.extensionVerificationBrowserExternal,
|
|
isSelected: normalizedMode == 'external_first',
|
|
onTap: () => onChanged('external_first'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ChannelChip(
|
|
label: context.l10n.extensionVerificationBrowserInApp,
|
|
isSelected: normalizedMode == 'in_app_first',
|
|
onTap: () => onChanged('in_app_first'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: 56,
|
|
endIndent: 20,
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ChannelChip extends StatelessWidget {
|
|
final String label;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
const _ChannelChip({
|
|
required this.label,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final unselectedColor = isDark
|
|
? Color.alphaBlend(
|
|
Colors.white.withValues(alpha: 0.05),
|
|
colorScheme.surface,
|
|
)
|
|
: colorScheme.surfaceContainerHigh;
|
|
return Expanded(
|
|
child: Material(
|
|
color: isSelected ? colorScheme.primaryContainer : unselectedColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: Center(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
|
color: isSelected
|
|
? colorScheme.onPrimaryContainer
|
|
: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|