mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-30 06:49:31 +00:00
feat: add shake detection functionality (#36)
* feat: add shake detection functionality and integrate vibration support * feat: add shake detector settings page
This commit is contained in:
parent
2e3b1de529
commit
b229c4f2f5
25 changed files with 1423 additions and 158 deletions
|
|
@ -12,6 +12,7 @@ import 'package:vaani/router/router.dart';
|
|||
import 'package:vaani/settings/app_settings_provider.dart';
|
||||
import 'package:vaani/settings/models/app_settings.dart' as model;
|
||||
import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart';
|
||||
|
||||
class AppSettingsPage extends HookConsumerWidget {
|
||||
const AppSettingsPage({
|
||||
|
|
@ -30,39 +31,6 @@ class AppSettingsPage extends HookConsumerWidget {
|
|||
return SimpleSettingsPage(
|
||||
title: const Text('App Settings'),
|
||||
sections: [
|
||||
// General section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: Text(
|
||||
'General',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: const Text('Notification Media Player'),
|
||||
leading: const Icon(Icons.play_lesson),
|
||||
description: const Text(
|
||||
'Customize the media player in notifications',
|
||||
),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.notificationSettings.name);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: const Text('Player Settings'),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
description: const Text(
|
||||
'Customize the player settings',
|
||||
),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.playerSettings.name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// Appearance section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
|
|
@ -106,20 +74,19 @@ class AppSettingsPage extends HookConsumerWidget {
|
|||
],
|
||||
),
|
||||
|
||||
// Sleep Timer section
|
||||
// General section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: Text(
|
||||
'Sleep Timer',
|
||||
'General',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
// initialValue: sleepTimerSettings.autoTurnOnTimer,
|
||||
title: const Text('Auto Turn On Timer'),
|
||||
NavigationWithSwitchTile(
|
||||
title: const Text('Auto Turn On Sleep Timer'),
|
||||
description: const Text(
|
||||
'Automatically turn on the sleep timer based on the time of day',
|
||||
),
|
||||
|
|
@ -127,39 +94,57 @@ class AppSettingsPage extends HookConsumerWidget {
|
|||
? const Icon(Icons.timer)
|
||||
: const Icon(Icons.timer_off),
|
||||
onPressed: (context) {
|
||||
// push the sleep timer settings page
|
||||
context.pushNamed(Routes.autoSleepTimerSettings.name);
|
||||
},
|
||||
// a switch to enable or disable the auto turn off time
|
||||
trailing: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||
indent: 8.0,
|
||||
endIndent: 8.0,
|
||||
// width: 8.0,
|
||||
// thickness: 2.0,
|
||||
// height: 24.0,
|
||||
),
|
||||
Switch(
|
||||
value: sleepTimerSettings.autoTurnOnTimer,
|
||||
onChanged: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings
|
||||
.sleepTimerSettings(
|
||||
autoTurnOnTimer: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
value: sleepTimerSettings.autoTurnOnTimer,
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.playerSettings.sleepTimerSettings(
|
||||
autoTurnOnTimer: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: const Text('Notification Media Player'),
|
||||
leading: const Icon(Icons.play_lesson),
|
||||
description: const Text(
|
||||
'Customize the media player in notifications',
|
||||
),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.notificationSettings.name);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: const Text('Player Settings'),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
description: const Text(
|
||||
'Customize the player settings',
|
||||
),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.playerSettings.name);
|
||||
},
|
||||
),
|
||||
NavigationWithSwitchTile(
|
||||
title: const Text('Shake Detector'),
|
||||
leading: const Icon(Icons.vibration),
|
||||
description: const Text(
|
||||
'Customize the shake detector settings',
|
||||
),
|
||||
value: appSettings.shakeDetectionSettings.isEnabled,
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.shakeDetectorSettings.name);
|
||||
},
|
||||
onToggle: (value) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.shakeDetectionSettings(
|
||||
isEnabled: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Backup and Restore section
|
||||
SettingsSection(
|
||||
margin: const EdgeInsetsDirectional.symmetric(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
|||
import 'package:vaani/settings/models/app_settings.dart';
|
||||
import 'package:vaani/settings/view/buttons.dart';
|
||||
import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||
import 'package:vaani/shared/extensions/enum.dart';
|
||||
|
||||
class NotificationSettingsPage extends HookConsumerWidget {
|
||||
const NotificationSettingsPage({
|
||||
|
|
@ -16,7 +17,7 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
|||
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: const Text('Notification Settings'),
|
||||
sections: [
|
||||
|
|
@ -37,7 +38,10 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
|||
children: [
|
||||
TextSpan(
|
||||
text: notificationSettings.primaryTitle,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -72,7 +76,10 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
|||
children: [
|
||||
TextSpan(
|
||||
text: notificationSettings.secondaryTitle,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -151,19 +158,17 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
|||
title: const Text('Media Controls'),
|
||||
leading: const Icon(Icons.control_camera),
|
||||
// description: const Text('Select the media controls to display'),
|
||||
description: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Select the media controls to display'),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
children: notificationSettings.mediaControls
|
||||
.map(
|
||||
(control) => Icon(control.icon),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
description: const Text('Select the media controls to display'),
|
||||
trailing: Wrap(
|
||||
spacing: 8.0,
|
||||
children: notificationSettings.mediaControls
|
||||
.map(
|
||||
(control) => Icon(
|
||||
control.icon,
|
||||
color: primaryColor,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
onPressed: (context) async {
|
||||
final selectedControls =
|
||||
|
|
@ -225,7 +230,7 @@ class MediaControlsPicker extends HookConsumerWidget {
|
|||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedMediaControls.value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
// a list of chips to easily select the media controls to display
|
||||
|
|
@ -235,14 +240,8 @@ class MediaControlsPicker extends HookConsumerWidget {
|
|||
children: NotificationMediaControl.values
|
||||
.map(
|
||||
(control) => ChoiceChip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(control.icon),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(control.name),
|
||||
],
|
||||
),
|
||||
avatar: Icon(control.icon),
|
||||
label: Text(control.pascalCase),
|
||||
selected: selectedMediaControls.value.contains(control),
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
|
|
@ -332,7 +331,7 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
|||
OkButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(selectedTitle.value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
// a list of chips to easily insert available fields into the text field
|
||||
|
|
@ -362,10 +361,10 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
|||
children: NotificationTitleType.values
|
||||
.map(
|
||||
(type) => ActionChip(
|
||||
label: Text(type.stringValue),
|
||||
label: Text(type.pascalCase),
|
||||
onPressed: () {
|
||||
final text = controller.text;
|
||||
final newText = '$text\$${type.stringValue}';
|
||||
final newText = '$text\$${type.name}';
|
||||
controller.text = newText;
|
||||
selectedTitle.value = newText;
|
||||
},
|
||||
|
|
@ -378,16 +377,3 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> showNotificationTitlePicker(
|
||||
BuildContext context, {
|
||||
required String initialValue,
|
||||
required String title,
|
||||
}) async {
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return NotificationTitlePicker(initialValue: initialValue, title: title);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
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'),
|
||||
|
|
@ -47,7 +48,11 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
// preferred default speed
|
||||
SettingsTile(
|
||||
title: const Text('Default Speed'),
|
||||
description: Text('${playerSettings.preferredDefaultSpeed}x'),
|
||||
trailing: Text(
|
||||
'${playerSettings.preferredDefaultSpeed}x',
|
||||
style:
|
||||
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: const Icon(Icons.speed),
|
||||
onPressed: (context) async {
|
||||
final newSpeed = await showDialog(
|
||||
|
|
@ -70,6 +75,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
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 {
|
||||
|
|
@ -104,7 +111,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
TextSpan(
|
||||
text: playerSettings
|
||||
.minimumPositionForReporting.smartBinaryFormat,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' of the book'),
|
||||
],
|
||||
|
|
@ -141,7 +151,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
TextSpan(
|
||||
text: playerSettings
|
||||
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' left in the book'),
|
||||
],
|
||||
|
|
@ -178,7 +191,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
|||
TextSpan(
|
||||
text: playerSettings
|
||||
.playbackReportInterval.smartBinaryFormat,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' to the server'),
|
||||
],
|
||||
|
|
|
|||
395
lib/settings/view/shake_detector_settings_page.dart
Normal file
395
lib/settings/view/shake_detector_settings_page.dart
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
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/models/app_settings.dart';
|
||||
import 'package:vaani/settings/view/buttons.dart';
|
||||
import 'package:vaani/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: const Text('Shake Detector Settings'),
|
||||
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: const Text('Enable Shake Detection'),
|
||||
description: const Text(
|
||||
'Enable shake detection to do various actions',
|
||||
),
|
||||
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: const Text('Shake Activation Threshold'),
|
||||
description: const Text(
|
||||
'The higher the threshold, the harder you need to shake',
|
||||
),
|
||||
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: const Text('Shake Action'),
|
||||
description: const Text(
|
||||
'The action to perform when a shake is detected',
|
||||
),
|
||||
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: const Text('Shake Feedback'),
|
||||
description: const Text(
|
||||
'The feedback to give when a shake is detected',
|
||||
),
|
||||
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: const Text('Select Shake Feedback'),
|
||||
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: const Text('Select Shake Action'),
|
||||
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: const Text('Select Shake Activation Threshold'),
|
||||
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: const Text(
|
||||
'Enter a number to set the threshold in m/s²',
|
||||
),
|
||||
),
|
||||
),
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
56
lib/settings/view/widgets/navigation_with_switch_tile.dart
Normal file
56
lib/settings/view/widgets/navigation_with_switch_tile.dart
Normal file
|
|
@ -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.withOpacity(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