mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
* feat: add shake detection functionality and integrate vibration support * feat: add shake detector settings page
426 lines
15 KiB
Dart
426 lines
15 KiB
Dart
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/settings/app_settings_provider.dart';
|
|
import 'package:vaani/settings/view/buttons.dart';
|
|
import 'package:vaani/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: const Text('Player Settings'),
|
|
sections: [
|
|
SettingsSection(
|
|
margin: const EdgeInsetsDirectional.symmetric(
|
|
horizontal: 16.0,
|
|
vertical: 8.0,
|
|
),
|
|
tiles: [
|
|
// preferred settings for every book
|
|
SettingsTile.switchTile(
|
|
title: const Text('Remember Player Settings for Every Book'),
|
|
leading: const Icon(Icons.settings_applications),
|
|
description: const Text(
|
|
'Settings like speed, loudness, etc. will be remembered for every book',
|
|
),
|
|
initialValue: playerSettings.configurePlayerForEveryBook,
|
|
onToggle: (value) {
|
|
ref.read(appSettingsProvider.notifier).update(
|
|
appSettings.copyWith.playerSettings(
|
|
configurePlayerForEveryBook: value,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
// preferred default speed
|
|
SettingsTile(
|
|
title: const Text('Default Speed'),
|
|
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: const Text('Speed Options'),
|
|
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: const Text('Playback Reporting'),
|
|
tiles: [
|
|
SettingsTile(
|
|
title: const Text('Minimum Position to Report'),
|
|
description: Text.rich(
|
|
TextSpan(
|
|
text: 'Do not report playback for the first ',
|
|
children: [
|
|
TextSpan(
|
|
text: playerSettings
|
|
.minimumPositionForReporting.smartBinaryFormat,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
const TextSpan(text: ' of the book'),
|
|
],
|
|
),
|
|
),
|
|
leading: const Icon(Icons.timer),
|
|
onPressed: (context) async {
|
|
final newDuration = await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return TimeDurationSelector(
|
|
title: const Text('Ignore Playback Position Less Than'),
|
|
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: const Text('Mark Complete When Time Left'),
|
|
description: Text.rich(
|
|
TextSpan(
|
|
text: 'Mark complete when less than ',
|
|
children: [
|
|
TextSpan(
|
|
text: playerSettings
|
|
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
const TextSpan(text: ' left in the book'),
|
|
],
|
|
),
|
|
),
|
|
leading: const Icon(Icons.cloud_done),
|
|
onPressed: (context) async {
|
|
final newDuration = await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return TimeDurationSelector(
|
|
title: const Text('Mark Complete When Time Left'),
|
|
baseUnit: BaseUnit.second,
|
|
initialValue: playerSettings.markCompleteWhenTimeLeft,
|
|
);
|
|
},
|
|
);
|
|
if (newDuration != null) {
|
|
ref.read(appSettingsProvider.notifier).update(
|
|
appSettings.copyWith.playerSettings(
|
|
markCompleteWhenTimeLeft: newDuration,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
// playback report interval
|
|
SettingsTile(
|
|
title: const Text('Playback Report Interval'),
|
|
description: Text.rich(
|
|
TextSpan(
|
|
text: 'Report progress every ',
|
|
children: [
|
|
TextSpan(
|
|
text: playerSettings
|
|
.playbackReportInterval.smartBinaryFormat,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
const TextSpan(text: ' to the server'),
|
|
],
|
|
),
|
|
),
|
|
leading: const Icon(Icons.change_circle_outlined),
|
|
onPressed: (context) async {
|
|
final newDuration = await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return TimeDurationSelector(
|
|
title: const Text('Playback Report Interval'),
|
|
baseUnit: BaseUnit.second,
|
|
initialValue: playerSettings.playbackReportInterval,
|
|
);
|
|
},
|
|
);
|
|
if (newDuration != null) {
|
|
ref.read(appSettingsProvider.notifier).update(
|
|
appSettings.copyWith.playerSettings(
|
|
playbackReportInterval: newDuration,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
// Display Settings
|
|
SettingsSection(
|
|
title: const Text('Display Settings'),
|
|
tiles: [
|
|
// show total progress
|
|
SettingsTile.switchTile(
|
|
title: const Text('Show Total Progress'),
|
|
leading: const Icon(Icons.show_chart),
|
|
description: const Text(
|
|
'Show the total progress of the book in the player',
|
|
),
|
|
initialValue:
|
|
playerSettings.expandedPlayerSettings.showTotalProgress,
|
|
onToggle: (value) {
|
|
ref.read(appSettingsProvider.notifier).update(
|
|
appSettings.copyWith.playerSettings
|
|
.expandedPlayerSettings(showTotalProgress: value),
|
|
);
|
|
},
|
|
),
|
|
// show chapter progress
|
|
SettingsTile.switchTile(
|
|
title: const Text('Show Chapter Progress'),
|
|
leading: const Icon(Icons.show_chart),
|
|
description: const Text(
|
|
'Show the progress of the current chapter in the player',
|
|
),
|
|
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: const Text('Select Speed'),
|
|
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: const InputDecoration(
|
|
labelText: 'Speed',
|
|
helper: Text(
|
|
'Enter the speed you want to set when playing for the first time',
|
|
),
|
|
),
|
|
),
|
|
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: const Text('Select Speed Options'),
|
|
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: const InputDecoration(
|
|
labelText: 'Add Speed Option',
|
|
helper: Text('Enter a new speed option to add'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
const CancelButton(),
|
|
OkButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(speedOptions.value);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|