mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 22:39:34 +00:00
一堆乱七八糟的修改
播放页面增加桌面版
This commit is contained in:
parent
aee1fbde88
commit
3ba35b31b8
116 changed files with 1238 additions and 2592 deletions
316
lib/features/settings/view/app_settings_page.dart
Normal file
316
lib/features/settings/view/app_settings_page.dart
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/router/router.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/models/app_settings.dart' as model;
|
||||
import 'package:vaani/features/settings/view/buttons.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/widgets/navigation_with_switch_tile.dart';
|
||||
|
||||
class AppSettingsPage extends HookConsumerWidget {
|
||||
const AppSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final sleepTimerSettings = appSettings.sleepTimerSettings;
|
||||
final locales = {'en': 'English', 'zh': '中文'};
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).appSettings),
|
||||
sections: [
|
||||
// General section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: Text(
|
||||
S.of(context).general,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).language),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
trailing: DropdownButton(
|
||||
value: appSettings.language,
|
||||
items: S.delegate.supportedLocales.map((locale) {
|
||||
return DropdownMenuItem(
|
||||
value: locale.languageCode,
|
||||
child: Text(locales[locale.languageCode] ?? 'unknown'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith(
|
||||
language: value!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
description: Text(S.of(context).languageDescription),
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettings),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
description: Text(S.of(context).playerSettingsDescription),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.playerSettings.name);
|
||||
},
|
||||
),
|
||||
NavigationWithSwitchTile(
|
||||
title: Text(S.of(context).autoTurnOnSleepTimer),
|
||||
description: Text(S.of(context).automaticallyDescription),
|
||||
leading: sleepTimerSettings.autoTurnOnTimer
|
||||
? const Icon(Icons.timer, fill: 1)
|
||||
: const Icon(Icons.timer_off, fill: 1),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.autoSleepTimerSettings.name);
|
||||
},
|
||||
value: sleepTimerSettings.autoTurnOnTimer,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.sleepTimerSettings(
|
||||
autoTurnOnTimer: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
NavigationWithSwitchTile(
|
||||
title: Text(S.of(context).shakeDetector),
|
||||
leading: const Icon(Icons.vibration),
|
||||
description: Text(
|
||||
S.of(context).shakeDetectorDescription,
|
||||
),
|
||||
value: appSettings.shakeDetectionSettings.isEnabled,
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.shakeDetectorSettings.name);
|
||||
},
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
isEnabled: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Appearance section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: Text(
|
||||
S.of(context).appearance,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
leading: const Icon(Icons.color_lens),
|
||||
title: Text(S.of(context).themeSettings),
|
||||
description: Text(S.of(context).themeSettingsDescription),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.themeSettings.name);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).notificationMediaPlayer),
|
||||
leading: const Icon(Icons.play_lesson),
|
||||
description:
|
||||
Text(S.of(context).notificationMediaPlayerDescription),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.notificationSettings.name);
|
||||
},
|
||||
),
|
||||
SettingsTile.navigation(
|
||||
leading: const Icon(Icons.home_filled),
|
||||
title: Text(S.of(context).homePageSettings),
|
||||
description: Text(S.of(context).homePageSettingsDescription),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.homePageSettings.name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Backup and Restore section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: Text(
|
||||
S.of(context).backupAndRestore,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).copyToClipboard),
|
||||
leading: const Icon(Icons.copy),
|
||||
description: Text(
|
||||
S.of(context).copyToClipboardDescription,
|
||||
),
|
||||
onPressed: (context) async {
|
||||
// copy to clipboard
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: jsonEncode(appSettings.toJson()),
|
||||
),
|
||||
);
|
||||
// show toast
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(S.of(context).copyToClipboardToast),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).restore),
|
||||
leading: const Icon(Icons.restore),
|
||||
description: Text(S.of(context).restoreDescription),
|
||||
onPressed: (context) {
|
||||
// show a dialog to get the backup
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RestoreDialogue();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// a button to reset the app settings
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).resetAppSettings),
|
||||
leading: const Icon(Icons.settings_backup_restore),
|
||||
description: Text(S.of(context).resetAppSettingsDescription),
|
||||
onPressed: (context) async {
|
||||
// confirm the reset
|
||||
final res = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).resetAppSettings),
|
||||
content: Text(S.of(context).resetAppSettingsDialog),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(S.of(context).cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(S.of(context).reset),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// if the user confirms the reset
|
||||
if (res == true) {
|
||||
ref.read(appSettingsProvider.notifier).reset();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestoreDialogue extends HookConsumerWidget {
|
||||
const RestoreDialogue({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final settings = useState<model.AppSettings?>(null);
|
||||
|
||||
final settingsInputController = useTextEditingController();
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).restoreBackup),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).backup,
|
||||
hintText: S.of(context).restoreBackupHint,
|
||||
// clear button
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
settingsInputController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return S.of(context).restoreBackupValidator;
|
||||
}
|
||||
try {
|
||||
// try to decode the backup
|
||||
settings.value = model.AppSettings.fromJson(
|
||||
jsonDecode(value),
|
||||
);
|
||||
} catch (e) {
|
||||
return S.of(context).restoreBackupInvalid;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
if (settings.value == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(S.of(context).restoreBackupInvalid),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ref.read(appSettingsProvider.notifier).update(settings.value!);
|
||||
settingsInputController.clear();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(S.of(context).restoreBackupSuccess),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(S.of(context).restoreBackupInvalid),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(S.of(context).restore),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
129
lib/features/settings/view/auto_sleep_timer_settings_page.dart
Normal file
129
lib/features/settings/view/auto_sleep_timer_settings_page.dart
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/shared/extensions/time_of_day.dart';
|
||||
|
||||
class AutoSleepTimerSettingsPage extends HookConsumerWidget {
|
||||
const AutoSleepTimerSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final sleepTimerSettings = appSettings.sleepTimerSettings;
|
||||
|
||||
var enabled = sleepTimerSettings.autoTurnOnTimer &&
|
||||
!sleepTimerSettings.alwaysAutoTurnOnTimer;
|
||||
final selectedValueColor = enabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).disabledColor;
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).autoSleepTimerSettings),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
// initialValue: sleepTimerSettings.autoTurnOnTimer,
|
||||
title: Text(S.of(context).autoTurnOnTimer),
|
||||
description: Text(
|
||||
S.of(context).autoTurnOnTimerDescription,
|
||||
),
|
||||
leading: sleepTimerSettings.autoTurnOnTimer
|
||||
? const Icon(Icons.timer_outlined)
|
||||
: const Icon(Icons.timer_off_outlined),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.sleepTimerSettings(
|
||||
autoTurnOnTimer: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
initialValue: sleepTimerSettings.autoTurnOnTimer,
|
||||
),
|
||||
// auto turn on time settings, enabled only when autoTurnOnTimer is enabled
|
||||
SettingsTile.navigation(
|
||||
enabled: enabled,
|
||||
leading: const Icon(Icons.play_circle),
|
||||
title: Text(S.of(context).autoTurnOnTimerFrom),
|
||||
description: Text(
|
||||
S.of(context).autoTurnOnTimerFromDescription,
|
||||
),
|
||||
onPressed: (context) async {
|
||||
// navigate to the time picker
|
||||
final selected = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
|
||||
);
|
||||
if (selected != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.sleepTimerSettings(
|
||||
autoTurnOnTime: selected.toDuration(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
trailing: Text(
|
||||
sleepTimerSettings.autoTurnOnTime.toTimeOfDay().format(context),
|
||||
style: TextStyle(color: selectedValueColor),
|
||||
),
|
||||
),
|
||||
SettingsTile.navigation(
|
||||
enabled: enabled,
|
||||
leading: const Icon(Icons.pause_circle),
|
||||
title: Text(S.of(context).autoTurnOnTimerUntil),
|
||||
description: Text(
|
||||
S.of(context).autoTurnOnTimerUntilDescription,
|
||||
),
|
||||
onPressed: (context) async {
|
||||
// navigate to the time picker
|
||||
final selected = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
|
||||
);
|
||||
if (selected != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.sleepTimerSettings(
|
||||
autoTurnOffTime: selected.toDuration(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
trailing: Text(
|
||||
sleepTimerSettings.autoTurnOffTime
|
||||
.toTimeOfDay()
|
||||
.format(context),
|
||||
style: TextStyle(color: selectedValueColor),
|
||||
),
|
||||
),
|
||||
|
||||
// switch tile for always auto turn on timer no matter what
|
||||
SettingsTile.switchTile(
|
||||
leading: const Icon(Icons.all_inclusive),
|
||||
title: Text(S.of(context).autoTurnOnTimerAlways),
|
||||
description: Text(
|
||||
S.of(context).autoTurnOnTimerAlwaysDescription,
|
||||
),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.sleepTimerSettings(
|
||||
alwaysAutoTurnOnTimer: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
enabled: sleepTimerSettings.autoTurnOnTimer,
|
||||
initialValue: sleepTimerSettings.alwaysAutoTurnOnTimer,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/features/settings/view/buttons.dart
Normal file
39
lib/features/settings/view/buttons.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
|
||||
class OkButton<T> extends StatelessWidget {
|
||||
const OkButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(S.of(context).ok),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CancelButton extends StatelessWidget {
|
||||
const CancelButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
onPressed?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(S.of(context).cancel),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/features/settings/view/home_page_settings_page.dart
Normal file
99
lib/features/settings/view/home_page_settings_page.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart'
|
||||
show SimpleSettingsPage;
|
||||
|
||||
class HomePageSettingsPage extends HookConsumerWidget {
|
||||
const HomePageSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final appSettingsNotifier = ref.read(appSettingsProvider.notifier);
|
||||
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).homePageSettings),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
title: Text(S.of(context).homePageSettingsQuickPlay),
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
initialValue: appSettings
|
||||
.homePageSettings.showPlayButtonOnContinueListeningShelf,
|
||||
title: Text(S.of(context).homeContinueListening),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
description:
|
||||
Text(S.of(context).homeBookContinueListeningDescription),
|
||||
onToggle: (value) {
|
||||
appSettingsNotifier.update(
|
||||
appSettings.copyWith(
|
||||
homePageSettings: appSettings.homePageSettings.copyWith(
|
||||
showPlayButtonOnContinueListeningShelf: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).homeBookContinueSeries),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
description:
|
||||
Text(S.of(context).homeBookContinueSeriesDescription),
|
||||
initialValue: appSettings
|
||||
.homePageSettings.showPlayButtonOnContinueSeriesShelf,
|
||||
onToggle: (value) {
|
||||
appSettingsNotifier.update(
|
||||
appSettings.copyWith(
|
||||
homePageSettings: appSettings.homePageSettings.copyWith(
|
||||
showPlayButtonOnContinueSeriesShelf: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).homePageSettingsOtherShelves),
|
||||
leading: const Icon(Icons.all_inclusive),
|
||||
description:
|
||||
Text(S.of(context).homePageSettingsOtherShelvesDescription),
|
||||
initialValue: appSettings
|
||||
.homePageSettings.showPlayButtonOnAllRemainingShelves,
|
||||
onToggle: (value) {
|
||||
appSettingsNotifier.update(
|
||||
appSettings.copyWith(
|
||||
homePageSettings: appSettings.homePageSettings.copyWith(
|
||||
showPlayButtonOnAllRemainingShelves: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).homeBookListenAgain),
|
||||
leading: const Icon(Icons.replay),
|
||||
description: Text(S.of(context).homeBookListenAgainDescription),
|
||||
initialValue:
|
||||
appSettings.homePageSettings.showPlayButtonOnListenAgainShelf,
|
||||
onToggle: (value) {
|
||||
appSettingsNotifier.update(
|
||||
appSettings.copyWith(
|
||||
homePageSettings: appSettings.homePageSettings.copyWith(
|
||||
showPlayButtonOnListenAgainShelf: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
385
lib/features/settings/view/notification_settings_page.dart
Normal file
385
lib/features/settings/view/notification_settings_page.dart
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/models/app_settings.dart';
|
||||
import 'package:vaani/features/settings/view/buttons.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/shared/extensions/enum.dart';
|
||||
|
||||
class NotificationSettingsPage extends HookConsumerWidget {
|
||||
const NotificationSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final notificationSettings = appSettings.notificationSettings;
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).notificationMediaPlayer),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.only(
|
||||
start: 16.0,
|
||||
end: 16.0,
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
// set the primary and secondary titles
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).nmpSettingsTitle),
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
text: S.of(context).nmpSettingsTitleDescription,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: notificationSettings.primaryTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.title),
|
||||
onPressed: (context) async {
|
||||
// show the notification title picker
|
||||
final selectedTitle = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return NotificationTitlePicker(
|
||||
initialValue: notificationSettings.primaryTitle,
|
||||
title: S.of(context).nmpSettingsTitle,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (selectedTitle != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
primaryTitle: selectedTitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).nmpSettingsSubTitle),
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
text: S.of(context).nmpSettingsSubTitleDescription,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: notificationSettings.secondaryTitle,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.title),
|
||||
onPressed: (context) async {
|
||||
// show the notification title picker
|
||||
final selectedTitle = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return NotificationTitlePicker(
|
||||
initialValue: notificationSettings.secondaryTitle,
|
||||
title: S.of(context).nmpSettingsSubTitle,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (selectedTitle != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
secondaryTitle: selectedTitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// set forward and backward intervals
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).nmpSettingsForward),
|
||||
description: Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).timeSecond(
|
||||
notificationSettings.fastForwardInterval.inSeconds,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TimeIntervalSlider(
|
||||
defaultValue: notificationSettings.fastForwardInterval,
|
||||
onChangedEnd: (interval) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
fastForwardInterval: interval,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: const Icon(Icons.fast_forward),
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).nmpSettingsBackward),
|
||||
description: Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).timeSecond(
|
||||
notificationSettings.rewindInterval.inSeconds,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TimeIntervalSlider(
|
||||
defaultValue: notificationSettings.rewindInterval,
|
||||
onChangedEnd: (interval) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
rewindInterval: interval,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: const Icon(Icons.fast_rewind),
|
||||
),
|
||||
// set the media controls
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).nmpSettingsMediaControls),
|
||||
leading: const Icon(Icons.control_camera),
|
||||
// description: const Text('Select the media controls to display'),
|
||||
description:
|
||||
Text(S.of(context).nmpSettingsMediaControlsDescription),
|
||||
trailing: Wrap(
|
||||
spacing: 8.0,
|
||||
children: notificationSettings.mediaControls
|
||||
.map(
|
||||
(control) => Icon(
|
||||
control.icon,
|
||||
color: primaryColor,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
onPressed: (context) async {
|
||||
final selectedControls =
|
||||
await showDialog<List<NotificationMediaControl>>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return MediaControlsPicker(
|
||||
selectedControls: notificationSettings.mediaControls,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (selectedControls != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
mediaControls: selectedControls,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// set the progress bar to show chapter progress
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).nmpSettingsShowChapterProgress),
|
||||
leading: const Icon(Icons.book),
|
||||
description:
|
||||
Text(S.of(context).nmpSettingsShowChapterProgressDescription),
|
||||
initialValue: notificationSettings.progressBarIsChapterProgress,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.notificationSettings(
|
||||
progressBarIsChapterProgress: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MediaControlsPicker extends HookConsumerWidget {
|
||||
const MediaControlsPicker({
|
||||
super.key,
|
||||
required this.selectedControls,
|
||||
});
|
||||
|
||||
final List<NotificationMediaControl> selectedControls;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedMediaControls = useState(selectedControls);
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).nmpSettingsMediaControls),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedMediaControls.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
// a list of chips to easily select the media controls to display
|
||||
// with icons and labels
|
||||
content: Wrap(
|
||||
spacing: 8.0,
|
||||
children: NotificationMediaControl.values
|
||||
.map(
|
||||
(control) => ChoiceChip(
|
||||
avatar: Icon(control.icon),
|
||||
label: Text(control.pascalCase),
|
||||
selected: selectedMediaControls.value.contains(control),
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
selectedMediaControls.value = [
|
||||
...selectedMediaControls.value,
|
||||
control,
|
||||
];
|
||||
} else {
|
||||
selectedMediaControls.value = [
|
||||
...selectedMediaControls.value.where((c) => c != control),
|
||||
];
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeIntervalSlider extends HookConsumerWidget {
|
||||
const TimeIntervalSlider({
|
||||
super.key,
|
||||
this.title,
|
||||
required this.defaultValue,
|
||||
this.onChanged,
|
||||
this.onChangedEnd,
|
||||
this.min = const Duration(seconds: 5),
|
||||
this.max = const Duration(seconds: 120),
|
||||
this.step = const Duration(seconds: 5),
|
||||
});
|
||||
|
||||
final Widget? title;
|
||||
final Duration defaultValue;
|
||||
final ValueChanged<Duration>? onChanged;
|
||||
final ValueChanged<Duration>? onChangedEnd;
|
||||
final Duration min;
|
||||
final Duration max;
|
||||
final Duration step;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedInterval = useState(defaultValue);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
title ?? const SizedBox.shrink(),
|
||||
if (title != null) const SizedBox(height: 8.0),
|
||||
Slider(
|
||||
value: selectedInterval.value.inSeconds.toDouble(),
|
||||
min: min.inSeconds.toDouble(),
|
||||
max: max.inSeconds.toDouble(),
|
||||
divisions: ((max.inSeconds - min.inSeconds) ~/ step.inSeconds),
|
||||
label: S.of(context).timeSecond(selectedInterval.value.inSeconds),
|
||||
onChanged: (value) {
|
||||
selectedInterval.value = Duration(seconds: value.toInt());
|
||||
onChanged?.call(selectedInterval.value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
onChangedEnd?.call(selectedInterval.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationTitlePicker extends HookConsumerWidget {
|
||||
const NotificationTitlePicker({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final String initialValue;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedTitle = useState(initialValue);
|
||||
final controller = useTextEditingController(text: initialValue);
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedTitle.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
// a list of chips to easily insert available fields into the text field
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
selectedTitle.value = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
helper: Text(S.of(context).nmpSettingsSelectOne),
|
||||
suffix: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
selectedTitle.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
children: NotificationTitleType.values
|
||||
.map(
|
||||
(type) => ActionChip(
|
||||
label: Text(type.pascalCase),
|
||||
onPressed: () {
|
||||
final text = controller.text;
|
||||
final newText = '$text\$${type.name}';
|
||||
controller.text = newText;
|
||||
selectedTitle.value = newText;
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
440
lib/features/settings/view/player_settings_page.dart
Normal file
440
lib/features/settings/view/player_settings_page.dart
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
import 'package:duration_picker/duration_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/view/buttons.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||
|
||||
class PlayerSettingsPage extends HookConsumerWidget {
|
||||
const PlayerSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final playerSettings = appSettings.playerSettings;
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).playerSettings),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
// preferred settings for every book
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).playerSettingsRememberForEveryBook),
|
||||
leading: const Icon(Icons.settings_applications),
|
||||
description: Text(
|
||||
S.of(context).playerSettingsRememberForEveryBookDescription,
|
||||
),
|
||||
initialValue: playerSettings.configurePlayerForEveryBook,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
configurePlayerForEveryBook: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// preferred default speed
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettingsSpeedDefault),
|
||||
trailing: Text(
|
||||
'${playerSettings.preferredDefaultSpeed}x',
|
||||
style:
|
||||
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: const Icon(Icons.speed),
|
||||
onPressed: (context) async {
|
||||
final newSpeed = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => SpeedPicker(
|
||||
initialValue: playerSettings.preferredDefaultSpeed,
|
||||
),
|
||||
);
|
||||
if (newSpeed != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
preferredDefaultSpeed: newSpeed,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// preferred speed options
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettingsSpeedOptions),
|
||||
description: Text(
|
||||
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
|
||||
),
|
||||
leading: const Icon(Icons.speed),
|
||||
onPressed: (context) async {
|
||||
final newSpeedOptions = await showDialog<List<double>?>(
|
||||
context: context,
|
||||
builder: (context) => SpeedOptionsPicker(
|
||||
initialValue: playerSettings.speedOptions,
|
||||
),
|
||||
);
|
||||
if (newSpeedOptions != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
speedOptions: newSpeedOptions..sort(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Playback Reporting
|
||||
SettingsSection(
|
||||
title: Text(S.of(context).playerSettingsPlaybackReporting),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettingsPlaybackReportingMinimum),
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
text: S
|
||||
.of(context)
|
||||
.playerSettingsPlaybackReportingMinimumDescriptionHead,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: playerSettings
|
||||
.minimumPositionForReporting.smartBinaryFormat,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: S
|
||||
.of(context)
|
||||
.playerSettingsPlaybackReportingMinimumDescriptionTail),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.timer),
|
||||
onPressed: (context) async {
|
||||
final newDuration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TimeDurationSelector(
|
||||
title: Text(
|
||||
S.of(context).playerSettingsPlaybackReportingIgnore),
|
||||
baseUnit: BaseUnit.second,
|
||||
initialValue: playerSettings.minimumPositionForReporting,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (newDuration != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
minimumPositionForReporting: newDuration,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// when to mark complete
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettingsCompleteTime),
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
text: S.of(context).playerSettingsCompleteTimeDescriptionHead,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: playerSettings
|
||||
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: S
|
||||
.of(context)
|
||||
.playerSettingsCompleteTimeDescriptionTail),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.cloud_done),
|
||||
onPressed: (context) async {
|
||||
final newDuration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TimeDurationSelector(
|
||||
title: Text(S.of(context).playerSettingsCompleteTime),
|
||||
baseUnit: BaseUnit.second,
|
||||
initialValue: playerSettings.markCompleteWhenTimeLeft,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (newDuration != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
markCompleteWhenTimeLeft: newDuration,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
// playback report interval
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).playerSettingsPlaybackInterval),
|
||||
description: Text.rich(
|
||||
TextSpan(
|
||||
text: S
|
||||
.of(context)
|
||||
.playerSettingsPlaybackIntervalDescriptionHead,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: playerSettings
|
||||
.playbackReportInterval.smartBinaryFormat,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: S
|
||||
.of(context)
|
||||
.playerSettingsPlaybackIntervalDescriptionTail),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.change_circle_outlined),
|
||||
onPressed: (context) async {
|
||||
final newDuration = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return TimeDurationSelector(
|
||||
title: Text(S.of(context).playerSettingsPlaybackInterval),
|
||||
baseUnit: BaseUnit.second,
|
||||
initialValue: playerSettings.playbackReportInterval,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (newDuration != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
playbackReportInterval: newDuration,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// Display Settings
|
||||
SettingsSection(
|
||||
title: Text(S.of(context).playerSettingsDisplay),
|
||||
tiles: [
|
||||
// show total progress
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).playerSettingsDisplayTotalProgress),
|
||||
leading: const Icon(Icons.show_chart),
|
||||
description: Text(
|
||||
S.of(context).playerSettingsDisplayTotalProgressDescription,
|
||||
),
|
||||
initialValue:
|
||||
playerSettings.expandedPlayerSettings.showTotalProgress,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings
|
||||
.expandedPlayerSettings(showTotalProgress: value),
|
||||
);
|
||||
},
|
||||
),
|
||||
// show chapter progress
|
||||
SettingsTile.switchTile(
|
||||
title: Text(S.of(context).playerSettingsDisplayChapterProgress),
|
||||
leading: const Icon(Icons.show_chart),
|
||||
description: Text(
|
||||
S.of(context).playerSettingsDisplayChapterProgressDescription,
|
||||
),
|
||||
initialValue:
|
||||
playerSettings.expandedPlayerSettings.showChapterProgress,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings(
|
||||
expandedPlayerSettings: playerSettings
|
||||
.expandedPlayerSettings
|
||||
.copyWith(showChapterProgress: value),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TimeDurationSelector extends HookConsumerWidget {
|
||||
const TimeDurationSelector({
|
||||
super.key,
|
||||
this.title = const Text('Select Duration'),
|
||||
this.baseUnit = BaseUnit.second,
|
||||
this.initialValue = Duration.zero,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final BaseUnit baseUnit;
|
||||
final Duration initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final duration = useState(initialValue);
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: DurationPicker(
|
||||
duration: duration.value,
|
||||
baseUnit: baseUnit,
|
||||
onChange: (value) {
|
||||
duration.value = value;
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(duration.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedPicker extends HookConsumerWidget {
|
||||
const SpeedPicker({
|
||||
super.key,
|
||||
this.initialValue = 1,
|
||||
});
|
||||
|
||||
final double initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final speedController =
|
||||
useTextEditingController(text: initialValue.toString());
|
||||
final speed = useState<double?>(initialValue);
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).playerSettingsSpeedSelect),
|
||||
content: TextField(
|
||||
controller: speedController,
|
||||
onChanged: (value) => speed.value = double.tryParse(value),
|
||||
onSubmitted: (value) {
|
||||
Navigator.of(context).pop(speed.value);
|
||||
},
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).playerSettingsSpeed,
|
||||
helper: Text(S.of(context).playerSettingsSpeedSelectHelper),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(speed.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedOptionsPicker extends HookConsumerWidget {
|
||||
const SpeedOptionsPicker({
|
||||
super.key,
|
||||
this.initialValue = const [0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
});
|
||||
|
||||
final List<double> initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final speedOptionAddController = useTextEditingController();
|
||||
final speedOptions = useState<List<double>>(initialValue);
|
||||
final focusNode = useFocusNode();
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).playerSettingsSpeedOptionsSelect),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: speedOptions.value
|
||||
.map(
|
||||
(speed) => Chip(
|
||||
label: Text('${speed}x'),
|
||||
onDeleted: speed == 1
|
||||
? null
|
||||
: () {
|
||||
speedOptions.value =
|
||||
speedOptions.value.where((element) {
|
||||
// speed option 1 can't be removed
|
||||
return element != speed;
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) {
|
||||
// if (a.label == const Text('1x')) {
|
||||
// return -1;
|
||||
// } else if (b.label == const Text('1x')) {
|
||||
// return 1;
|
||||
// }
|
||||
return a.label.toString().compareTo(b.label.toString());
|
||||
}),
|
||||
),
|
||||
TextField(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
controller: speedOptionAddController,
|
||||
onSubmitted: (value) {
|
||||
final newSpeed = double.tryParse(value);
|
||||
if (newSpeed != null && !speedOptions.value.contains(newSpeed)) {
|
||||
speedOptions.value = [...speedOptions.value, newSpeed];
|
||||
}
|
||||
speedOptionAddController.clear();
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).playerSettingsSpeedOptionsSelectAdd,
|
||||
helper:
|
||||
Text(S.of(context).playerSettingsSpeedOptionsSelectAddHelper),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(speedOptions.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
396
lib/features/settings/view/shake_detector_settings_page.dart
Normal file
396
lib/features/settings/view/shake_detector_settings_page.dart
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/models/app_settings.dart';
|
||||
import 'package:vaani/features/settings/view/buttons.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/shared/extensions/enum.dart';
|
||||
|
||||
class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||
const ShakeDetectorSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final shakeDetectionSettings = appSettings.shakeDetectionSettings;
|
||||
final isShakeDetectionEnabled = shakeDetectionSettings.isEnabled;
|
||||
final selectedValueColor = isShakeDetectionEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).disabledColor;
|
||||
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).shakeDetectorSettings),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
leading: shakeDetectionSettings.isEnabled
|
||||
? const Icon(Icons.vibration)
|
||||
: const Icon(Icons.not_interested),
|
||||
title: Text(S.of(context).shakeDetectorEnable),
|
||||
description: Text(
|
||||
S.of(context).shakeDetectorEnableDescription,
|
||||
),
|
||||
initialValue: shakeDetectionSettings.isEnabled,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
isEnabled: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Shake Detection Settings
|
||||
SettingsSection(
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
enabled: isShakeDetectionEnabled,
|
||||
leading: const Icon(Icons.flag_circle),
|
||||
title: Text(S.of(context).shakeActivationThreshold),
|
||||
description: Text(
|
||||
S.of(context).shakeActivationThresholdDescription,
|
||||
),
|
||||
trailing: Text(
|
||||
'${shakeDetectionSettings.threshold} m/s²',
|
||||
style: TextStyle(
|
||||
color: selectedValueColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onPressed: (context) async {
|
||||
final newThreshold = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => ShakeForceSelector(
|
||||
initialValue: shakeDetectionSettings.threshold,
|
||||
),
|
||||
);
|
||||
|
||||
if (newThreshold != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
threshold: newThreshold,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// shake action
|
||||
SettingsTile(
|
||||
enabled: isShakeDetectionEnabled,
|
||||
leading: const Icon(Icons.directions_run),
|
||||
title: Text(S.of(context).shakeAction),
|
||||
description: Text(
|
||||
S.of(context).shakeActionDescription,
|
||||
),
|
||||
trailing: Icon(
|
||||
shakeDetectionSettings.shakeAction.icon,
|
||||
color: selectedValueColor,
|
||||
),
|
||||
onPressed: (context) async {
|
||||
final newShakeAction = await showDialog<ShakeAction>(
|
||||
context: context,
|
||||
builder: (context) => ShakeActionSelector(
|
||||
initialValue: shakeDetectionSettings.shakeAction,
|
||||
),
|
||||
);
|
||||
|
||||
if (newShakeAction != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
shakeAction: newShakeAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// shake feedback
|
||||
SettingsTile(
|
||||
enabled: isShakeDetectionEnabled,
|
||||
leading: const Icon(Icons.feedback),
|
||||
title: Text(S.of(context).shakeFeedback),
|
||||
description: Text(
|
||||
S.of(context).shakeFeedbackDescription,
|
||||
),
|
||||
trailing: shakeDetectionSettings.feedback.isEmpty
|
||||
? Icon(
|
||||
Icons.not_interested,
|
||||
color: Theme.of(context).disabledColor,
|
||||
)
|
||||
: Wrap(
|
||||
spacing: 8.0,
|
||||
children: shakeDetectionSettings.feedback.map(
|
||||
(feedback) {
|
||||
return Icon(
|
||||
feedback.icon,
|
||||
color: selectedValueColor,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
onPressed: (context) async {
|
||||
final newFeedback =
|
||||
await showDialog<Set<ShakeDetectedFeedback>>(
|
||||
context: context,
|
||||
builder: (context) => ShakeFeedbackSelector(
|
||||
initialValue: shakeDetectionSettings.feedback,
|
||||
),
|
||||
);
|
||||
|
||||
if (newFeedback != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
feedback: newFeedback,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShakeFeedbackSelector extends HookConsumerWidget {
|
||||
const ShakeFeedbackSelector({
|
||||
super.key,
|
||||
this.initialValue = const {ShakeDetectedFeedback.vibrate},
|
||||
});
|
||||
|
||||
final Set<ShakeDetectedFeedback> initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final feedback = useState(initialValue);
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).shakeSelectFeedback),
|
||||
content: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: ShakeDetectedFeedback.values
|
||||
.map(
|
||||
(feedbackType) => ChoiceChip(
|
||||
avatar: Icon(feedbackType.icon),
|
||||
label: Text(feedbackType.pascalCase),
|
||||
tooltip: feedbackType.description,
|
||||
onSelected: (val) {
|
||||
if (feedback.value.contains(feedbackType)) {
|
||||
feedback.value = feedback.value
|
||||
.where((element) => element != feedbackType)
|
||||
.toSet();
|
||||
} else {
|
||||
feedback.value = {...feedback.value, feedbackType};
|
||||
}
|
||||
},
|
||||
selected: feedback.value.contains(feedbackType),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(feedback.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShakeActionSelector extends HookConsumerWidget {
|
||||
const ShakeActionSelector({
|
||||
super.key,
|
||||
this.initialValue = ShakeAction.resetSleepTimer,
|
||||
});
|
||||
|
||||
final ShakeAction initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final shakeAction = useState(initialValue);
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).shakeSelectAction),
|
||||
content: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: ShakeAction.values
|
||||
.map(
|
||||
// chips with radio buttons as one of the options can be selected
|
||||
(action) => ChoiceChip(
|
||||
avatar: Icon(action.icon),
|
||||
label: Text(action.pascalCase),
|
||||
onSelected: (val) {
|
||||
shakeAction.value = action;
|
||||
},
|
||||
selected: shakeAction.value == action,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(shakeAction.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShakeForceSelector extends HookConsumerWidget {
|
||||
const ShakeForceSelector({
|
||||
super.key,
|
||||
this.initialValue = 6,
|
||||
});
|
||||
|
||||
final double initialValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final shakeForce = useState(initialValue);
|
||||
final controller = useTextEditingController(text: initialValue.toString());
|
||||
return AlertDialog(
|
||||
title: Text(S.of(context).shakeSelectActivationThreshold),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
final newThreshold = double.tryParse(value);
|
||||
if (newThreshold != null) {
|
||||
shakeForce.value = newThreshold;
|
||||
}
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
// clear button
|
||||
suffix: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
shakeForce.value = 0;
|
||||
},
|
||||
),
|
||||
helper: Text(
|
||||
S.of(context).shakeSelectActivationThresholdHelper,
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: ShakeForce.values
|
||||
.map(
|
||||
(force) => ChoiceChip(
|
||||
label: Text(force.pascalCase),
|
||||
onSelected: (val) {
|
||||
controller.text = force.threshold.toString();
|
||||
shakeForce.value = force.threshold;
|
||||
},
|
||||
selected: shakeForce.value == force.threshold,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(shakeForce.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShakeForce {
|
||||
whisper(0.5),
|
||||
low(2.5),
|
||||
medium(5),
|
||||
high(7.5),
|
||||
storm(10),
|
||||
hurricane(15),
|
||||
earthquake(20),
|
||||
meteorShower(30),
|
||||
supernova(40),
|
||||
blackHole(50);
|
||||
|
||||
const ShakeForce(this.threshold);
|
||||
|
||||
final double threshold;
|
||||
}
|
||||
|
||||
extension ShakeActionIcon on ShakeAction {
|
||||
IconData? get icon {
|
||||
switch (this) {
|
||||
case ShakeAction.none:
|
||||
return Icons.not_interested;
|
||||
case ShakeAction.resetSleepTimer:
|
||||
return Icons.timer;
|
||||
case ShakeAction.playPause:
|
||||
return Icons.play_arrow;
|
||||
// case ShakeAction.nextChapter:
|
||||
// return Icons.skip_next;
|
||||
// case ShakeAction.previousChapter:
|
||||
// return Icons.skip_previous;
|
||||
// case ShakeAction.volumeUp:
|
||||
// return Icons.volume_up;
|
||||
// case ShakeAction.volumeDown:
|
||||
// return Icons.volume_down;
|
||||
case ShakeAction.fastForward:
|
||||
return Icons.fast_forward;
|
||||
case ShakeAction.rewind:
|
||||
return Icons.fast_rewind;
|
||||
default:
|
||||
return Icons.question_mark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on ShakeDetectedFeedback {
|
||||
IconData? get icon {
|
||||
switch (this) {
|
||||
case ShakeDetectedFeedback.vibrate:
|
||||
return Icons.vibration;
|
||||
case ShakeDetectedFeedback.beep:
|
||||
return Icons.volume_up;
|
||||
default:
|
||||
return Icons.question_mark;
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case ShakeDetectedFeedback.vibrate:
|
||||
return 'Vibrate the device';
|
||||
case ShakeDetectedFeedback.beep:
|
||||
return 'Play a beep sound';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
57
lib/features/settings/view/simple_settings_page.dart
Normal file
57
lib/features/settings/view/simple_settings_page.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
|
||||
|
||||
class SimpleSettingsPage extends HookConsumerWidget {
|
||||
const SimpleSettingsPage({
|
||||
super.key,
|
||||
this.title,
|
||||
this.sections,
|
||||
});
|
||||
|
||||
final Widget? title;
|
||||
final List<AbstractSettingsSection>? sections;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: title,
|
||||
// ),
|
||||
// body: body,
|
||||
// an app bar which is bigger than the default app bar but on scroll shrinks to the default app bar with the title being animated
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 200.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: title,
|
||||
// background: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
if (sections != null)
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
child: SettingsList(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
sections: sections!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// some padding at the bottom
|
||||
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
|
||||
SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
203
lib/features/settings/view/theme_settings_page.dart
Normal file
203
lib/features/settings/view/theme_settings_page.dart
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:vaani/generated/l10n.dart';
|
||||
import 'package:vaani/features/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/features/settings/view/simple_settings_page.dart';
|
||||
|
||||
class ThemeSettingsPage extends HookConsumerWidget {
|
||||
const ThemeSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final themeSettings = appSettings.themeSettings;
|
||||
// final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return SimpleSettingsPage(
|
||||
title: Text(S.of(context).themeSettings),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
tiles: [
|
||||
// choose system , light or dark theme
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).themeMode),
|
||||
description: SegmentedButton(
|
||||
expandedInsets: const EdgeInsets.only(top: 8.0),
|
||||
showSelectedIcon: true,
|
||||
selectedIcon: const Icon(Icons.check),
|
||||
selected: {themeSettings.themeMode},
|
||||
onSelectionChanged: (newSelection) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.themeSettings(
|
||||
themeMode: newSelection.first,
|
||||
),
|
||||
);
|
||||
},
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
icon: Icon(Icons.light_mode),
|
||||
label: Text(S.of(context).themeModeLight),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
icon: Icon(Icons.auto_awesome),
|
||||
label: Text(S.of(context).themeModeSystem),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
icon: Icon(Icons.dark_mode),
|
||||
label: Text(S.of(context).themeModeDark),
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: Icon(
|
||||
themeSettings.themeMode == ThemeMode.light
|
||||
? Icons.light_mode
|
||||
: themeSettings.themeMode == ThemeMode.dark
|
||||
? Icons.dark_mode
|
||||
: Icons.auto_awesome,
|
||||
),
|
||||
),
|
||||
|
||||
// high contrast mode
|
||||
SettingsTile.switchTile(
|
||||
leading: themeSettings.highContrast
|
||||
? const Icon(Icons.accessibility)
|
||||
: const Icon(Icons.accessibility_new_outlined),
|
||||
initialValue: themeSettings.highContrast,
|
||||
title: Text(S.of(context).themeModeHighContrast),
|
||||
description: Text(
|
||||
S.of(context).themeModeHighContrastDescription,
|
||||
),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.themeSettings(
|
||||
highContrast: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// use material theme from system
|
||||
SettingsTile.switchTile(
|
||||
initialValue: themeSettings.useMaterialThemeFromSystem,
|
||||
title: Platform.isAndroid
|
||||
? Text(S.of(context).themeSettingsColorsAndroid)
|
||||
: Text(S.of(context).themeSettingsColors),
|
||||
description: Text(S.of(context).themeSettingsColorsDescription),
|
||||
leading: themeSettings.useMaterialThemeFromSystem
|
||||
? const Icon(Icons.auto_awesome)
|
||||
: const Icon(Icons.auto_fix_off),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.themeSettings(
|
||||
useMaterialThemeFromSystem: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// TODO choose the primary color
|
||||
// SettingsTile.navigation(
|
||||
// title: const Text('Primary Color'),
|
||||
// description: const Text(
|
||||
// 'Choose the primary color for the app',
|
||||
// ),
|
||||
// leading: const Icon(Icons.colorize),
|
||||
// trailing: Icon(
|
||||
// Icons.circle,
|
||||
// color: themeSettings.customThemeColor.toColor(),
|
||||
// ),
|
||||
// onPressed: (context) async {
|
||||
// final selectedColor = await showDialog<Color>(
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return SimpleDialog(
|
||||
// title: const Text('Select Primary Color'),
|
||||
// children: [
|
||||
// for (final color in Colors.primaries)
|
||||
// SimpleDialogOption(
|
||||
// onPressed: () {
|
||||
// Navigator.pop(context, color);
|
||||
// },
|
||||
// child: Container(
|
||||
// color: color,
|
||||
// height: 48,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// if (selectedColor != null) {
|
||||
// ref.read(appSettingsProvider.notifier).update(
|
||||
// appSettings.copyWith.themeSettings(
|
||||
// customThemeColor: selectedColor.toHexString(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
|
||||
// use theme throughout the app when playing item
|
||||
SettingsTile.switchTile(
|
||||
initialValue: themeSettings.useCurrentPlayerThemeThroughoutApp,
|
||||
title: Text(S.of(context).themeSettingsColorsCurrent),
|
||||
description:
|
||||
Text(S.of(context).themeSettingsColorsCurrentDescription),
|
||||
leading: themeSettings.useCurrentPlayerThemeThroughoutApp
|
||||
? const Icon(Icons.auto_fix_high)
|
||||
: const Icon(Icons.auto_fix_off),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.themeSettings(
|
||||
useCurrentPlayerThemeThroughoutApp: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SettingsTile.switchTile(
|
||||
initialValue: themeSettings.useMaterialThemeOnItemPage,
|
||||
title: Text(S.of(context).themeSettingsColorsBook),
|
||||
description:
|
||||
Text(S.of(context).themeSettingsColorsBookDescription),
|
||||
leading: themeSettings.useMaterialThemeOnItemPage
|
||||
? const Icon(Icons.auto_fix_high)
|
||||
: const Icon(Icons.auto_fix_off),
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.themeSettings(
|
||||
useMaterialThemeOnItemPage: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ColorExtension on Color {
|
||||
String toHexString() {
|
||||
return '#${value.toRadixString(16).substring(2)}';
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
Color toColor() {
|
||||
return Color(int.parse('0xff$substring(1)'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||
|
||||
class NavigationWithSwitchTile extends AbstractSettingsTile {
|
||||
const NavigationWithSwitchTile({
|
||||
this.leading,
|
||||
// this.trailing,
|
||||
required this.value,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.descriptionInlineIos = false,
|
||||
this.onPressed,
|
||||
this.enabled = true,
|
||||
this.backgroundColor,
|
||||
super.key,
|
||||
this.onToggle,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Widget? description;
|
||||
final Color? backgroundColor;
|
||||
final bool descriptionInlineIos;
|
||||
final bool enabled;
|
||||
final Widget? leading;
|
||||
final Function(BuildContext)? onPressed;
|
||||
final bool value;
|
||||
final Function(bool)? onToggle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsTile.navigation(
|
||||
title: title,
|
||||
description: description,
|
||||
backgroundColor: backgroundColor,
|
||||
descriptionInlineIos: descriptionInlineIos,
|
||||
enabled: enabled,
|
||||
leading: leading,
|
||||
onPressed: onPressed,
|
||||
trailing: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).dividerColor.withValues(alpha: 0.5),
|
||||
indent: 8.0,
|
||||
endIndent: 8.0,
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: value,
|
||||
onChanged: onToggle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue