diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 41b686e..cb0a2a5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
element.stringValue == type)
+ .firstWhere((element) => element.name == type)
.extractFrom(book) ??
match.group(0) ??
'';
diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart
index 3c8ee41..933121c 100644
--- a/lib/features/player/view/player_when_expanded.dart
+++ b/lib/features/player/view/player_when_expanded.dart
@@ -11,6 +11,7 @@ import 'package:vaani/features/sleep_timer/core/sleep_timer.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider;
import 'package:vaani/settings/app_settings_provider.dart';
+import 'package:vaani/shared/extensions/duration_format.dart';
import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/not_implemented.dart';
@@ -376,8 +377,8 @@ class RemainingSleepTimeDisplay extends HookConsumerWidget {
),
child: Text(
timer.timer == null
- ? timer.duration.formatSingleLargestUnit()
- : remainingTime?.formatSingleLargestUnit() ?? '',
+ ? timer.duration.smartBinaryFormat
+ : remainingTime?.smartBinaryFormat ?? '',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
@@ -385,17 +386,3 @@ class RemainingSleepTimeDisplay extends HookConsumerWidget {
);
}
}
-
-extension DurationFormat on Duration {
- /// will return a number followed by h, m, or s depending on the duration
- /// only the largest unit will be shown
- String formatSingleLargestUnit() {
- if (inHours > 0) {
- return '${inHours}h';
- } else if (inMinutes > 0) {
- return '${inMinutes}m';
- } else {
- return '${inSeconds}s';
- }
- }
-}
diff --git a/lib/features/shake_detection/core/shake_detector.dart b/lib/features/shake_detection/core/shake_detector.dart
new file mode 100644
index 0000000..4b7b719
--- /dev/null
+++ b/lib/features/shake_detection/core/shake_detector.dart
@@ -0,0 +1,79 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:logging/logging.dart';
+import 'package:sensors_plus/sensors_plus.dart';
+import 'package:vaani/settings/models/app_settings.dart';
+
+final _logger = Logger('ShakeDetector');
+
+class ShakeDetector {
+ final ShakeDetectionSettings _settings;
+ final Function()? onShakeDetected;
+
+ ShakeDetector(
+ this._settings,
+ this.onShakeDetected, {
+ startImmediately = true,
+ }) {
+ _logger.fine('ShakeDetector created with settings: $_settings');
+ if (startImmediately) {
+ start();
+ }
+ }
+
+ StreamSubscription? _accelerometerSubscription;
+
+ int _currentShakeCount = 0;
+
+ DateTime _lastShakeTime = DateTime.now();
+
+ void start() {
+ if (_accelerometerSubscription != null) {
+ _logger.warning('ShakeDetector is already running');
+ return;
+ }
+ _accelerometerSubscription =
+ userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod)
+ .listen((event) {
+ if (event.rms > _settings.threshold) {
+ _currentShakeCount++;
+
+ if (_currentShakeCount >= _settings.shakeTriggerCount &&
+ !isCoolDownNeeded()) {
+ _logger.fine('Shake detected $_currentShakeCount times');
+
+ onShakeDetected?.call();
+
+ _lastShakeTime = DateTime.now();
+ _currentShakeCount = 0;
+ }
+ } else {
+ _currentShakeCount = 0;
+ }
+ });
+
+ _logger.fine('ShakeDetector started');
+ }
+
+ void stop() {
+ _currentShakeCount = 0;
+ _accelerometerSubscription?.cancel();
+ _accelerometerSubscription = null;
+ _logger.fine('ShakeDetector stopped');
+ }
+
+ void dispose() {
+ stop();
+ }
+
+ bool isCoolDownNeeded() {
+ return _lastShakeTime
+ .add(_settings.shakeTriggerCoolDown)
+ .isAfter(DateTime.now());
+ }
+}
+
+extension UserAccelerometerEventRMS on UserAccelerometerEvent {
+ double get rms => sqrt(x * x + y * y + z * z);
+}
diff --git a/lib/features/shake_detection/providers/shake_detector.dart b/lib/features/shake_detection/providers/shake_detector.dart
new file mode 100644
index 0000000..956fc16
--- /dev/null
+++ b/lib/features/shake_detection/providers/shake_detector.dart
@@ -0,0 +1,138 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:just_audio/just_audio.dart';
+import 'package:logging/logging.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:vaani/features/player/providers/audiobook_player.dart'
+ show audiobookPlayerProvider, simpleAudiobookPlayerProvider;
+import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
+ show sleepTimerProvider;
+import 'package:vaani/settings/app_settings_provider.dart'
+ show appSettingsProvider;
+import 'package:vaani/settings/models/app_settings.dart';
+import 'package:vibration/vibration.dart';
+
+import '../core/shake_detector.dart' as core;
+
+part 'shake_detector.g.dart';
+
+Logger _logger = Logger('ShakeDetector');
+
+@riverpod
+class ShakeDetector extends _$ShakeDetector {
+ @override
+ core.ShakeDetector? build() {
+ final appSettings = ref.watch(appSettingsProvider);
+ final shakeDetectionSettings = appSettings.shakeDetectionSettings;
+
+ if (!shakeDetectionSettings.isEnabled) {
+ _logger.fine('Shake detection is disabled');
+ return null;
+ }
+ final player = ref.watch(audiobookPlayerProvider);
+ if (!player.playing && !shakeDetectionSettings.isActiveWhenPaused) {
+ _logger.fine(
+ 'Shake detection is disabled when paused and player is not playing',
+ );
+ return null;
+ }
+ // if sleep timer is not enabled, shake detection should not be enabled
+ final sleepTimer = ref.watch(sleepTimerProvider);
+ if (!shakeDetectionSettings.isPlaybackManagementEnabled &&
+ sleepTimer == null) {
+ _logger.fine('No playback management is enabled and sleep timer is off, '
+ 'so shake detection is disabled');
+ return null;
+ }
+
+ _logger.fine('Creating shake detector');
+ final detector = core.ShakeDetector(
+ shakeDetectionSettings,
+ () {
+ doShakeAction(
+ shakeDetectionSettings.shakeAction,
+ ref: ref,
+ );
+ shakeDetectionSettings.feedback.forEach(postShakeFeedback);
+ },
+ );
+ ref.onDispose(detector.dispose);
+ return detector;
+ }
+
+ void doShakeAction(
+ ShakeAction shakeAction, {
+ required Ref ref,
+ }) {
+ final player = ref.watch(simpleAudiobookPlayerProvider);
+ switch (shakeAction) {
+ case ShakeAction.resetSleepTimer:
+ _logger.fine('Resetting sleep timer');
+ ref.read(sleepTimerProvider.notifier).restartTimer();
+ break;
+ case ShakeAction.fastForward:
+ _logger.fine('Fast forwarding');
+ player.seek(player.position + const Duration(seconds: 30));
+ break;
+ case ShakeAction.rewind:
+ _logger.fine('Rewinding');
+ player.seek(player.position - const Duration(seconds: 30));
+ break;
+ case ShakeAction.playPause:
+ if (player.book == null) {
+ _logger.warning('No book is loaded');
+ break;
+ }
+ player.togglePlayPause();
+ break;
+ default:
+ break;
+ }
+ }
+
+ Future postShakeFeedback(ShakeDetectedFeedback feedback) async {
+ switch (feedback) {
+ case ShakeDetectedFeedback.vibrate:
+ _logger.fine('Vibrating');
+
+ if (await Vibration.hasAmplitudeControl() ?? false) {
+ Vibration.vibrate(amplitude: 128, duration: 200);
+ break;
+ }
+
+ if (await Vibration.hasVibrator() ?? false) {
+ Vibration.vibrate();
+ break;
+ }
+
+ _logger.warning('No vibration support');
+
+ break;
+ case ShakeDetectedFeedback.beep:
+ _logger.fine('Beeping');
+ final player = AudioPlayer();
+ await player.setAsset('assets/sounds/beep.mp3');
+ await player.setVolume(0.5);
+ await player.play();
+ await player.dispose();
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+extension on ShakeDetectionSettings {
+ bool get isActiveWhenPaused {
+ // If the shake action is play/pause, it should be required when not playing
+ return shakeAction == ShakeAction.playPause;
+ }
+
+ bool get isPlaybackManagementEnabled {
+ return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind}
+ .contains(shakeAction);
+ }
+
+ bool get shouldActOnSleepTimer {
+ return {ShakeAction.resetSleepTimer}.contains(shakeAction);
+ }
+}
diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detection/providers/shake_detector.g.dart
new file mode 100644
index 0000000..c3035b8
--- /dev/null
+++ b/lib/features/shake_detection/providers/shake_detector.g.dart
@@ -0,0 +1,26 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'shake_detector.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$shakeDetectorHash() => r'7bfbdd22f2f43ef3e3858d226d1eb78923e8114d';
+
+/// See also [ShakeDetector].
+@ProviderFor(ShakeDetector)
+final shakeDetectorProvider =
+ AutoDisposeNotifierProvider.internal(
+ ShakeDetector.new,
+ name: r'shakeDetectorProvider',
+ debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+ ? null
+ : _$shakeDetectorHash,
+ dependencies: null,
+ allTransitiveDependencies: null,
+);
+
+typedef _$ShakeDetector = AutoDisposeNotifier;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart
index fb38e0f..728cec1 100644
--- a/lib/features/sleep_timer/core/sleep_timer.dart
+++ b/lib/features/sleep_timer/core/sleep_timer.dart
@@ -19,7 +19,7 @@ class SleepTimer {
set duration(Duration value) {
_duration = value;
- reset();
+ clearCountDownTimer();
}
/// The player to be paused
@@ -40,7 +40,7 @@ class SleepTimer {
player.playbackEventStream.listen((event) {
if (event.processingState == ProcessingState.completed ||
event.processingState == ProcessingState.idle) {
- reset();
+ clearCountDownTimer();
}
}),
);
@@ -49,9 +49,9 @@ class SleepTimer {
_subscriptions.add(
player.playerStateStream.listen((state) {
if (state.playing && timer == null) {
- startTimer();
+ startCountDown();
} else if (!state.playing) {
- reset();
+ clearCountDownTimer();
}
}),
);
@@ -59,8 +59,8 @@ class SleepTimer {
_logger.fine('created with duration: $duration');
}
- /// resets the timer
- void reset() {
+ /// resets the timer and stops it
+ void clearCountDownTimer() {
if (timer != null) {
timer!.cancel();
_logger.fine(
@@ -70,15 +70,25 @@ class SleepTimer {
}
}
+ /// refills the timer with the default duration and starts it if the player is playing
+ /// if the player is not playing, the timer is stopped
+ void restartTimer() {
+ clearCountDownTimer();
+ if (player.playing) {
+ startCountDown();
+ }
+ _logger.fine('restarted timer');
+ }
+
/// starts the timer with the given duration or the default duration
- void startTimer([
+ void startCountDown([
Duration? forDuration,
]) {
- reset();
+ clearCountDownTimer();
duration = forDuration ?? duration;
timer = Timer(duration, () {
player.pause();
- reset();
+ clearCountDownTimer();
_logger.fine('paused player after $duration');
});
startedAt = DateTime.now();
@@ -103,7 +113,7 @@ class SleepTimer {
/// dispose the timer
void dispose() {
- reset();
+ clearCountDownTimer();
for (var sub in _subscriptions) {
sub.cancel();
}
diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart
index 563c34e..6f1b1b7 100644
--- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart
+++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
-import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'
- as core;
+import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/time_of_day.dart';
@@ -48,9 +47,16 @@ class SleepTimer extends _$SleepTimer {
);
ref.onDispose(timer.dispose);
state = timer;
+ state!.startCountDown();
}
}
+ void restartTimer() {
+ state?.restartTimer();
+
+ ref.notifyListeners();
+ }
+
void cancelTimer() {
state?.dispose();
state = null;
diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart
index ef930bf..fe74010 100644
--- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart
+++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart
@@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator
// **************************************************************************
-String _$sleepTimerHash() => r'ad77e82c1b513bbc62815c64ce1ed403f92fc055';
+String _$sleepTimerHash() => r'9d9f20267da91e5483151b58b7d4d7c0762c3ca7';
/// See also [SleepTimer].
@ProviderFor(SleepTimer)
diff --git a/lib/main.dart b/lib/main.dart
index cea6f7f..3228a8a 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -7,6 +7,7 @@ import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart';
import 'package:vaani/features/player/core/init.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
+import 'package:vaani/features/shake_detection/providers/shake_detector.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/settings/api_settings_provider.dart';
@@ -93,6 +94,7 @@ class _EagerInitialization extends ConsumerWidget {
ref.watch(sleepTimerProvider);
ref.watch(playbackReporterProvider);
ref.watch(simpleDownloadManagerProvider);
+ ref.watch(shakeDetectorProvider);
} catch (e) {
debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
}
diff --git a/lib/router/constants.dart b/lib/router/constants.dart
index 71bb446..4d8e7c1 100644
--- a/lib/router/constants.dart
+++ b/lib/router/constants.dart
@@ -42,6 +42,11 @@ class Routes {
name: 'playerSettings',
parentRoute: settings,
);
+ static const shakeDetectorSettings = _SimpleRoute(
+ pathName: 'shakeDetector',
+ name: 'shakeDetectorSettings',
+ parentRoute: settings,
+ );
// search and explore
static const search = _SimpleRoute(
diff --git a/lib/router/router.dart b/lib/router/router.dart
index edf9433..8910fc3 100644
--- a/lib/router/router.dart
+++ b/lib/router/router.dart
@@ -14,6 +14,7 @@ import 'package:vaani/settings/view/app_settings_page.dart';
import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart';
import 'package:vaani/settings/view/notification_settings_page.dart';
import 'package:vaani/settings/view/player_settings_page.dart';
+import 'package:vaani/settings/view/shake_detector_settings_page.dart';
import 'scaffold_with_nav_bar.dart';
import 'transitions/slide.dart';
@@ -195,6 +196,13 @@ class MyAppRouter {
pageBuilder:
defaultPageBuilder(const PlayerSettingsPage()),
),
+ GoRoute(
+ path: Routes.shakeDetectorSettings.pathName,
+ name: Routes.shakeDetectorSettings.name,
+ pageBuilder: defaultPageBuilder(
+ const ShakeDetectorSettingsPage(),
+ ),
+ ),
],
),
GoRoute(
diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart
index 4b70f64..09a07e2 100644
--- a/lib/settings/models/app_settings.dart
+++ b/lib/settings/models/app_settings.dart
@@ -16,6 +16,8 @@ class AppSettings with _$AppSettings {
@Default(PlayerSettings()) PlayerSettings playerSettings,
@Default(DownloadSettings()) DownloadSettings downloadSettings,
@Default(NotificationSettings()) NotificationSettings notificationSettings,
+ @Default(ShakeDetectionSettings())
+ ShakeDetectionSettings shakeDetectionSettings,
}) = _AppSettings;
factory AppSettings.fromJson(Map json) =>
@@ -165,17 +167,13 @@ class NotificationSettings with _$NotificationSettings {
}
enum NotificationTitleType {
- chapterTitle('chapterTitle'),
- bookTitle('bookTitle'),
- author('author'),
- subtitle('subtitle'),
- series('series'),
- narrator('narrator'),
- year('year');
-
- const NotificationTitleType(this.stringValue);
-
- final String stringValue;
+ chapterTitle,
+ bookTitle,
+ author,
+ subtitle,
+ series,
+ narrator,
+ year,
}
enum NotificationMediaControl {
@@ -190,3 +188,41 @@ enum NotificationMediaControl {
final IconData icon;
}
+
+/// Shake Detection Settings
+@freezed
+class ShakeDetectionSettings with _$ShakeDetectionSettings {
+ const factory ShakeDetectionSettings({
+ @Default(true) bool isEnabled,
+ @Default(ShakeDirection.horizontal) ShakeDirection direction,
+ @Default(5) double threshold,
+ @Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction,
+ @Default({ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep})
+ Set feedback,
+ @Default(0.5) double beepVolume,
+
+ /// the duration to wait before the shake detection is enabled again
+ @Default(Duration(seconds: 5)) Duration shakeTriggerCoolDown,
+
+ /// the number of shakes required to trigger the action
+ @Default(2) int shakeTriggerCount,
+
+ /// acceleration sampling interval
+ @Default(Duration(milliseconds: 100)) Duration samplingPeriod,
+ }) = _ShakeDetectionSettings;
+
+ factory ShakeDetectionSettings.fromJson(Map json) =>
+ _$ShakeDetectionSettingsFromJson(json);
+}
+
+enum ShakeDirection { horizontal, vertical }
+
+enum ShakeAction {
+ none,
+ playPause,
+ resetSleepTimer,
+ fastForward,
+ rewind,
+}
+
+enum ShakeDetectedFeedback { vibrate, beep }
diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart
index 415a60a..3c1d9f4 100644
--- a/lib/settings/models/app_settings.freezed.dart
+++ b/lib/settings/models/app_settings.freezed.dart
@@ -25,6 +25,8 @@ mixin _$AppSettings {
DownloadSettings get downloadSettings => throw _privateConstructorUsedError;
NotificationSettings get notificationSettings =>
throw _privateConstructorUsedError;
+ ShakeDetectionSettings get shakeDetectionSettings =>
+ throw _privateConstructorUsedError;
/// Serializes this AppSettings to a JSON map.
Map toJson() => throw _privateConstructorUsedError;
@@ -46,12 +48,14 @@ abstract class $AppSettingsCopyWith<$Res> {
{ThemeSettings themeSettings,
PlayerSettings playerSettings,
DownloadSettings downloadSettings,
- NotificationSettings notificationSettings});
+ NotificationSettings notificationSettings,
+ ShakeDetectionSettings shakeDetectionSettings});
$ThemeSettingsCopyWith<$Res> get themeSettings;
$PlayerSettingsCopyWith<$Res> get playerSettings;
$DownloadSettingsCopyWith<$Res> get downloadSettings;
$NotificationSettingsCopyWith<$Res> get notificationSettings;
+ $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;
}
/// @nodoc
@@ -73,6 +77,7 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
Object? playerSettings = null,
Object? downloadSettings = null,
Object? notificationSettings = null,
+ Object? shakeDetectionSettings = null,
}) {
return _then(_value.copyWith(
themeSettings: null == themeSettings
@@ -91,6 +96,10 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
? _value.notificationSettings
: notificationSettings // ignore: cast_nullable_to_non_nullable
as NotificationSettings,
+ shakeDetectionSettings: null == shakeDetectionSettings
+ ? _value.shakeDetectionSettings
+ : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable
+ as ShakeDetectionSettings,
) as $Val);
}
@@ -134,6 +143,17 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
return _then(_value.copyWith(notificationSettings: value) as $Val);
});
}
+
+ /// Create a copy of AppSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @pragma('vm:prefer-inline')
+ $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings {
+ return $ShakeDetectionSettingsCopyWith<$Res>(_value.shakeDetectionSettings,
+ (value) {
+ return _then(_value.copyWith(shakeDetectionSettings: value) as $Val);
+ });
+ }
}
/// @nodoc
@@ -148,7 +168,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res>
{ThemeSettings themeSettings,
PlayerSettings playerSettings,
DownloadSettings downloadSettings,
- NotificationSettings notificationSettings});
+ NotificationSettings notificationSettings,
+ ShakeDetectionSettings shakeDetectionSettings});
@override
$ThemeSettingsCopyWith<$Res> get themeSettings;
@@ -158,6 +179,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res>
$DownloadSettingsCopyWith<$Res> get downloadSettings;
@override
$NotificationSettingsCopyWith<$Res> get notificationSettings;
+ @override
+ $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;
}
/// @nodoc
@@ -177,6 +200,7 @@ class __$$AppSettingsImplCopyWithImpl<$Res>
Object? playerSettings = null,
Object? downloadSettings = null,
Object? notificationSettings = null,
+ Object? shakeDetectionSettings = null,
}) {
return _then(_$AppSettingsImpl(
themeSettings: null == themeSettings
@@ -195,6 +219,10 @@ class __$$AppSettingsImplCopyWithImpl<$Res>
? _value.notificationSettings
: notificationSettings // ignore: cast_nullable_to_non_nullable
as NotificationSettings,
+ shakeDetectionSettings: null == shakeDetectionSettings
+ ? _value.shakeDetectionSettings
+ : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable
+ as ShakeDetectionSettings,
));
}
}
@@ -206,7 +234,8 @@ class _$AppSettingsImpl implements _AppSettings {
{this.themeSettings = const ThemeSettings(),
this.playerSettings = const PlayerSettings(),
this.downloadSettings = const DownloadSettings(),
- this.notificationSettings = const NotificationSettings()});
+ this.notificationSettings = const NotificationSettings(),
+ this.shakeDetectionSettings = const ShakeDetectionSettings()});
factory _$AppSettingsImpl.fromJson(Map json) =>
_$$AppSettingsImplFromJson(json);
@@ -223,10 +252,13 @@ class _$AppSettingsImpl implements _AppSettings {
@override
@JsonKey()
final NotificationSettings notificationSettings;
+ @override
+ @JsonKey()
+ final ShakeDetectionSettings shakeDetectionSettings;
@override
String toString() {
- return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings)';
+ return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings)';
}
@override
@@ -241,13 +273,15 @@ class _$AppSettingsImpl implements _AppSettings {
(identical(other.downloadSettings, downloadSettings) ||
other.downloadSettings == downloadSettings) &&
(identical(other.notificationSettings, notificationSettings) ||
- other.notificationSettings == notificationSettings));
+ other.notificationSettings == notificationSettings) &&
+ (identical(other.shakeDetectionSettings, shakeDetectionSettings) ||
+ other.shakeDetectionSettings == shakeDetectionSettings));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, themeSettings, playerSettings,
- downloadSettings, notificationSettings);
+ downloadSettings, notificationSettings, shakeDetectionSettings);
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -270,7 +304,8 @@ abstract class _AppSettings implements AppSettings {
{final ThemeSettings themeSettings,
final PlayerSettings playerSettings,
final DownloadSettings downloadSettings,
- final NotificationSettings notificationSettings}) = _$AppSettingsImpl;
+ final NotificationSettings notificationSettings,
+ final ShakeDetectionSettings shakeDetectionSettings}) = _$AppSettingsImpl;
factory _AppSettings.fromJson(Map json) =
_$AppSettingsImpl.fromJson;
@@ -283,6 +318,8 @@ abstract class _AppSettings implements AppSettings {
DownloadSettings get downloadSettings;
@override
NotificationSettings get notificationSettings;
+ @override
+ ShakeDetectionSettings get shakeDetectionSettings;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -2381,3 +2418,377 @@ abstract class _NotificationSettings implements NotificationSettings {
_$$NotificationSettingsImplCopyWith<_$NotificationSettingsImpl>
get copyWith => throw _privateConstructorUsedError;
}
+
+ShakeDetectionSettings _$ShakeDetectionSettingsFromJson(
+ Map json) {
+ return _ShakeDetectionSettings.fromJson(json);
+}
+
+/// @nodoc
+mixin _$ShakeDetectionSettings {
+ bool get isEnabled => throw _privateConstructorUsedError;
+ ShakeDirection get direction => throw _privateConstructorUsedError;
+ double get threshold => throw _privateConstructorUsedError;
+ ShakeAction get shakeAction => throw _privateConstructorUsedError;
+ Set get feedback => throw _privateConstructorUsedError;
+ double get beepVolume => throw _privateConstructorUsedError;
+
+ /// the duration to wait before the shake detection is enabled again
+ Duration get shakeTriggerCoolDown => throw _privateConstructorUsedError;
+
+ /// the number of shakes required to trigger the action
+ int get shakeTriggerCount => throw _privateConstructorUsedError;
+
+ /// acceleration sampling interval
+ Duration get samplingPeriod => throw _privateConstructorUsedError;
+
+ /// Serializes this ShakeDetectionSettings to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of ShakeDetectionSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $ShakeDetectionSettingsCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $ShakeDetectionSettingsCopyWith<$Res> {
+ factory $ShakeDetectionSettingsCopyWith(ShakeDetectionSettings value,
+ $Res Function(ShakeDetectionSettings) then) =
+ _$ShakeDetectionSettingsCopyWithImpl<$Res, ShakeDetectionSettings>;
+ @useResult
+ $Res call(
+ {bool isEnabled,
+ ShakeDirection direction,
+ double threshold,
+ ShakeAction shakeAction,
+ Set feedback,
+ double beepVolume,
+ Duration shakeTriggerCoolDown,
+ int shakeTriggerCount,
+ Duration samplingPeriod});
+}
+
+/// @nodoc
+class _$ShakeDetectionSettingsCopyWithImpl<$Res,
+ $Val extends ShakeDetectionSettings>
+ implements $ShakeDetectionSettingsCopyWith<$Res> {
+ _$ShakeDetectionSettingsCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of ShakeDetectionSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isEnabled = null,
+ Object? direction = null,
+ Object? threshold = null,
+ Object? shakeAction = null,
+ Object? feedback = null,
+ Object? beepVolume = null,
+ Object? shakeTriggerCoolDown = null,
+ Object? shakeTriggerCount = null,
+ Object? samplingPeriod = null,
+ }) {
+ return _then(_value.copyWith(
+ isEnabled: null == isEnabled
+ ? _value.isEnabled
+ : isEnabled // ignore: cast_nullable_to_non_nullable
+ as bool,
+ direction: null == direction
+ ? _value.direction
+ : direction // ignore: cast_nullable_to_non_nullable
+ as ShakeDirection,
+ threshold: null == threshold
+ ? _value.threshold
+ : threshold // ignore: cast_nullable_to_non_nullable
+ as double,
+ shakeAction: null == shakeAction
+ ? _value.shakeAction
+ : shakeAction // ignore: cast_nullable_to_non_nullable
+ as ShakeAction,
+ feedback: null == feedback
+ ? _value.feedback
+ : feedback // ignore: cast_nullable_to_non_nullable
+ as Set,
+ beepVolume: null == beepVolume
+ ? _value.beepVolume
+ : beepVolume // ignore: cast_nullable_to_non_nullable
+ as double,
+ shakeTriggerCoolDown: null == shakeTriggerCoolDown
+ ? _value.shakeTriggerCoolDown
+ : shakeTriggerCoolDown // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ shakeTriggerCount: null == shakeTriggerCount
+ ? _value.shakeTriggerCount
+ : shakeTriggerCount // ignore: cast_nullable_to_non_nullable
+ as int,
+ samplingPeriod: null == samplingPeriod
+ ? _value.samplingPeriod
+ : samplingPeriod // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$ShakeDetectionSettingsImplCopyWith<$Res>
+ implements $ShakeDetectionSettingsCopyWith<$Res> {
+ factory _$$ShakeDetectionSettingsImplCopyWith(
+ _$ShakeDetectionSettingsImpl value,
+ $Res Function(_$ShakeDetectionSettingsImpl) then) =
+ __$$ShakeDetectionSettingsImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {bool isEnabled,
+ ShakeDirection direction,
+ double threshold,
+ ShakeAction shakeAction,
+ Set feedback,
+ double beepVolume,
+ Duration shakeTriggerCoolDown,
+ int shakeTriggerCount,
+ Duration samplingPeriod});
+}
+
+/// @nodoc
+class __$$ShakeDetectionSettingsImplCopyWithImpl<$Res>
+ extends _$ShakeDetectionSettingsCopyWithImpl<$Res,
+ _$ShakeDetectionSettingsImpl>
+ implements _$$ShakeDetectionSettingsImplCopyWith<$Res> {
+ __$$ShakeDetectionSettingsImplCopyWithImpl(
+ _$ShakeDetectionSettingsImpl _value,
+ $Res Function(_$ShakeDetectionSettingsImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of ShakeDetectionSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? isEnabled = null,
+ Object? direction = null,
+ Object? threshold = null,
+ Object? shakeAction = null,
+ Object? feedback = null,
+ Object? beepVolume = null,
+ Object? shakeTriggerCoolDown = null,
+ Object? shakeTriggerCount = null,
+ Object? samplingPeriod = null,
+ }) {
+ return _then(_$ShakeDetectionSettingsImpl(
+ isEnabled: null == isEnabled
+ ? _value.isEnabled
+ : isEnabled // ignore: cast_nullable_to_non_nullable
+ as bool,
+ direction: null == direction
+ ? _value.direction
+ : direction // ignore: cast_nullable_to_non_nullable
+ as ShakeDirection,
+ threshold: null == threshold
+ ? _value.threshold
+ : threshold // ignore: cast_nullable_to_non_nullable
+ as double,
+ shakeAction: null == shakeAction
+ ? _value.shakeAction
+ : shakeAction // ignore: cast_nullable_to_non_nullable
+ as ShakeAction,
+ feedback: null == feedback
+ ? _value._feedback
+ : feedback // ignore: cast_nullable_to_non_nullable
+ as Set,
+ beepVolume: null == beepVolume
+ ? _value.beepVolume
+ : beepVolume // ignore: cast_nullable_to_non_nullable
+ as double,
+ shakeTriggerCoolDown: null == shakeTriggerCoolDown
+ ? _value.shakeTriggerCoolDown
+ : shakeTriggerCoolDown // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ shakeTriggerCount: null == shakeTriggerCount
+ ? _value.shakeTriggerCount
+ : shakeTriggerCount // ignore: cast_nullable_to_non_nullable
+ as int,
+ samplingPeriod: null == samplingPeriod
+ ? _value.samplingPeriod
+ : samplingPeriod // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$ShakeDetectionSettingsImpl implements _ShakeDetectionSettings {
+ const _$ShakeDetectionSettingsImpl(
+ {this.isEnabled = true,
+ this.direction = ShakeDirection.horizontal,
+ this.threshold = 5,
+ this.shakeAction = ShakeAction.resetSleepTimer,
+ final Set feedback = const {
+ ShakeDetectedFeedback.vibrate,
+ ShakeDetectedFeedback.beep
+ },
+ this.beepVolume = 0.5,
+ this.shakeTriggerCoolDown = const Duration(seconds: 5),
+ this.shakeTriggerCount = 2,
+ this.samplingPeriod = const Duration(milliseconds: 100)})
+ : _feedback = feedback;
+
+ factory _$ShakeDetectionSettingsImpl.fromJson(Map json) =>
+ _$$ShakeDetectionSettingsImplFromJson(json);
+
+ @override
+ @JsonKey()
+ final bool isEnabled;
+ @override
+ @JsonKey()
+ final ShakeDirection direction;
+ @override
+ @JsonKey()
+ final double threshold;
+ @override
+ @JsonKey()
+ final ShakeAction shakeAction;
+ final Set _feedback;
+ @override
+ @JsonKey()
+ Set get feedback {
+ if (_feedback is EqualUnmodifiableSetView) return _feedback;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableSetView(_feedback);
+ }
+
+ @override
+ @JsonKey()
+ final double beepVolume;
+
+ /// the duration to wait before the shake detection is enabled again
+ @override
+ @JsonKey()
+ final Duration shakeTriggerCoolDown;
+
+ /// the number of shakes required to trigger the action
+ @override
+ @JsonKey()
+ final int shakeTriggerCount;
+
+ /// acceleration sampling interval
+ @override
+ @JsonKey()
+ final Duration samplingPeriod;
+
+ @override
+ String toString() {
+ return 'ShakeDetectionSettings(isEnabled: $isEnabled, direction: $direction, threshold: $threshold, shakeAction: $shakeAction, feedback: $feedback, beepVolume: $beepVolume, shakeTriggerCoolDown: $shakeTriggerCoolDown, shakeTriggerCount: $shakeTriggerCount, samplingPeriod: $samplingPeriod)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$ShakeDetectionSettingsImpl &&
+ (identical(other.isEnabled, isEnabled) ||
+ other.isEnabled == isEnabled) &&
+ (identical(other.direction, direction) ||
+ other.direction == direction) &&
+ (identical(other.threshold, threshold) ||
+ other.threshold == threshold) &&
+ (identical(other.shakeAction, shakeAction) ||
+ other.shakeAction == shakeAction) &&
+ const DeepCollectionEquality().equals(other._feedback, _feedback) &&
+ (identical(other.beepVolume, beepVolume) ||
+ other.beepVolume == beepVolume) &&
+ (identical(other.shakeTriggerCoolDown, shakeTriggerCoolDown) ||
+ other.shakeTriggerCoolDown == shakeTriggerCoolDown) &&
+ (identical(other.shakeTriggerCount, shakeTriggerCount) ||
+ other.shakeTriggerCount == shakeTriggerCount) &&
+ (identical(other.samplingPeriod, samplingPeriod) ||
+ other.samplingPeriod == samplingPeriod));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ isEnabled,
+ direction,
+ threshold,
+ shakeAction,
+ const DeepCollectionEquality().hash(_feedback),
+ beepVolume,
+ shakeTriggerCoolDown,
+ shakeTriggerCount,
+ samplingPeriod);
+
+ /// Create a copy of ShakeDetectionSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$ShakeDetectionSettingsImplCopyWith<_$ShakeDetectionSettingsImpl>
+ get copyWith => __$$ShakeDetectionSettingsImplCopyWithImpl<
+ _$ShakeDetectionSettingsImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$ShakeDetectionSettingsImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _ShakeDetectionSettings implements ShakeDetectionSettings {
+ const factory _ShakeDetectionSettings(
+ {final bool isEnabled,
+ final ShakeDirection direction,
+ final double threshold,
+ final ShakeAction shakeAction,
+ final Set feedback,
+ final double beepVolume,
+ final Duration shakeTriggerCoolDown,
+ final int shakeTriggerCount,
+ final Duration samplingPeriod}) = _$ShakeDetectionSettingsImpl;
+
+ factory _ShakeDetectionSettings.fromJson(Map json) =
+ _$ShakeDetectionSettingsImpl.fromJson;
+
+ @override
+ bool get isEnabled;
+ @override
+ ShakeDirection get direction;
+ @override
+ double get threshold;
+ @override
+ ShakeAction get shakeAction;
+ @override
+ Set get feedback;
+ @override
+ double get beepVolume;
+
+ /// the duration to wait before the shake detection is enabled again
+ @override
+ Duration get shakeTriggerCoolDown;
+
+ /// the number of shakes required to trigger the action
+ @override
+ int get shakeTriggerCount;
+
+ /// acceleration sampling interval
+ @override
+ Duration get samplingPeriod;
+
+ /// Create a copy of ShakeDetectionSettings
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$ShakeDetectionSettingsImplCopyWith<_$ShakeDetectionSettingsImpl>
+ get copyWith => throw _privateConstructorUsedError;
+}
diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart
index c01610a..3b0784f 100644
--- a/lib/settings/models/app_settings.g.dart
+++ b/lib/settings/models/app_settings.g.dart
@@ -24,6 +24,10 @@ _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) =>
? const NotificationSettings()
: NotificationSettings.fromJson(
json['notificationSettings'] as Map),
+ shakeDetectionSettings: json['shakeDetectionSettings'] == null
+ ? const ShakeDetectionSettings()
+ : ShakeDetectionSettings.fromJson(
+ json['shakeDetectionSettings'] as Map),
);
Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
@@ -32,6 +36,7 @@ Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
'playerSettings': instance.playerSettings,
'downloadSettings': instance.downloadSettings,
'notificationSettings': instance.notificationSettings,
+ 'shakeDetectionSettings': instance.shakeDetectionSettings,
};
_$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) =>
@@ -274,3 +279,63 @@ const _$NotificationMediaControlEnumMap = {
NotificationMediaControl.skipToNextChapter: 'skipToNextChapter',
NotificationMediaControl.skipToPreviousChapter: 'skipToPreviousChapter',
};
+
+_$ShakeDetectionSettingsImpl _$$ShakeDetectionSettingsImplFromJson(
+ Map json) =>
+ _$ShakeDetectionSettingsImpl(
+ isEnabled: json['isEnabled'] as bool? ?? true,
+ direction:
+ $enumDecodeNullable(_$ShakeDirectionEnumMap, json['direction']) ??
+ ShakeDirection.horizontal,
+ threshold: (json['threshold'] as num?)?.toDouble() ?? 5,
+ shakeAction:
+ $enumDecodeNullable(_$ShakeActionEnumMap, json['shakeAction']) ??
+ ShakeAction.resetSleepTimer,
+ feedback: (json['feedback'] as List?)
+ ?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e))
+ .toSet() ??
+ const {ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep},
+ beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5,
+ shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null
+ ? const Duration(seconds: 5)
+ : Duration(
+ microseconds: (json['shakeTriggerCoolDown'] as num).toInt()),
+ shakeTriggerCount: (json['shakeTriggerCount'] as num?)?.toInt() ?? 2,
+ samplingPeriod: json['samplingPeriod'] == null
+ ? const Duration(milliseconds: 100)
+ : Duration(microseconds: (json['samplingPeriod'] as num).toInt()),
+ );
+
+Map _$$ShakeDetectionSettingsImplToJson(
+ _$ShakeDetectionSettingsImpl instance) =>
+ {
+ 'isEnabled': instance.isEnabled,
+ 'direction': _$ShakeDirectionEnumMap[instance.direction]!,
+ 'threshold': instance.threshold,
+ 'shakeAction': _$ShakeActionEnumMap[instance.shakeAction]!,
+ 'feedback': instance.feedback
+ .map((e) => _$ShakeDetectedFeedbackEnumMap[e]!)
+ .toList(),
+ 'beepVolume': instance.beepVolume,
+ 'shakeTriggerCoolDown': instance.shakeTriggerCoolDown.inMicroseconds,
+ 'shakeTriggerCount': instance.shakeTriggerCount,
+ 'samplingPeriod': instance.samplingPeriod.inMicroseconds,
+ };
+
+const _$ShakeDirectionEnumMap = {
+ ShakeDirection.horizontal: 'horizontal',
+ ShakeDirection.vertical: 'vertical',
+};
+
+const _$ShakeActionEnumMap = {
+ ShakeAction.none: 'none',
+ ShakeAction.playPause: 'playPause',
+ ShakeAction.resetSleepTimer: 'resetSleepTimer',
+ ShakeAction.fastForward: 'fastForward',
+ ShakeAction.rewind: 'rewind',
+};
+
+const _$ShakeDetectedFeedbackEnumMap = {
+ ShakeDetectedFeedback.vibrate: 'vibrate',
+ ShakeDetectedFeedback.beep: 'beep',
+};
diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart
index bbdf1a3..58754c6 100644
--- a/lib/settings/view/app_settings_page.dart
+++ b/lib/settings/view/app_settings_page.dart
@@ -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(
diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart
index b673add..140d21c 100644
--- a/lib/settings/view/notification_settings_page.dart
+++ b/lib/settings/view/notification_settings_page.dart
@@ -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 showNotificationTitlePicker(
- BuildContext context, {
- required String initialValue,
- required String title,
-}) async {
- return showDialog(
- context: context,
- builder: (context) {
- return NotificationTitlePicker(initialValue: initialValue, title: title);
- },
- );
-}
diff --git a/lib/settings/view/player_settings_page.dart b/lib/settings/view/player_settings_page.dart
index c07f8cf..58b050f 100644
--- a/lib/settings/view/player_settings_page.dart
+++ b/lib/settings/view/player_settings_page.dart
@@ -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'),
],
diff --git a/lib/settings/view/shake_detector_settings_page.dart b/lib/settings/view/shake_detector_settings_page.dart
new file mode 100644
index 0000000..98c5b8d
--- /dev/null
+++ b/lib/settings/view/shake_detector_settings_page.dart
@@ -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(
+ 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(
+ 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>(
+ 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 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';
+ }
+ }
+}
diff --git a/lib/settings/view/widgets/navigation_with_switch_tile.dart b/lib/settings/view/widgets/navigation_with_switch_tile.dart
new file mode 100644
index 0000000..dbc8ecf
--- /dev/null
+++ b/lib/settings/view/widgets/navigation_with_switch_tile.dart
@@ -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,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/shared/extensions/duration_format.dart b/lib/shared/extensions/duration_format.dart
index d871424..09906cf 100644
--- a/lib/shared/extensions/duration_format.dart
+++ b/lib/shared/extensions/duration_format.dart
@@ -16,6 +16,16 @@ extension DurationFormat on Duration {
return '${seconds}s';
}
}
+
+ String get smartSingleFormat {
+ if (inHours > 0) {
+ return '${inHours}h';
+ } else if (inMinutes > 0) {
+ return '${inMinutes}m';
+ } else {
+ return '${inSeconds}s';
+ }
+ }
}
extension OnlyTime on DateTime {
diff --git a/lib/shared/extensions/enum.dart b/lib/shared/extensions/enum.dart
new file mode 100644
index 0000000..e0e6c09
--- /dev/null
+++ b/lib/shared/extensions/enum.dart
@@ -0,0 +1,25 @@
+extension TitleCase on Enum {
+ String get properName {
+ final name = toString().split('.').last;
+ return name[0].toUpperCase() + name.substring(1);
+ }
+
+ String get titleCase {
+ return name
+ .replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}')
+ .trim();
+ }
+
+ String get pascalCase {
+ // capitalize the first letter of each word
+ return name
+ .replaceAllMapped(
+ RegExp(r'([A-Z])'),
+ (match) => ' ${match.group(0)}',
+ )
+ .trim()
+ .split(' ')
+ .map((word) => word[0].toUpperCase() + word.substring(1))
+ .join(' ');
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index f7059ef..969bcca 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1363,6 +1363,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
+ vibration:
+ dependency: "direct main"
+ description:
+ name: vibration
+ sha256: fe8f90e1827f86a4f722b819799ecac8a24789a39c6d562ea316bcaeb8b1ec61
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ vibration_platform_interface:
+ dependency: transitive
+ description:
+ name: vibration_platform_interface
+ sha256: "735a5fef0f284de0ad9449a5ed7d36ba017c6f59b5b20ac64418af4a6bd35ee7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.1"
vm_service:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 11eddba..8bbeef6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -88,6 +88,7 @@ dependencies:
path: ./shelfsdk
shimmer: ^3.0.0
url_launcher: ^6.2.6
+ vibration: ^2.0.0
dev_dependencies:
build_runner: ^2.4.9
custom_lint: ^0.6.4
@@ -112,6 +113,7 @@ flutter:
assets:
- assets/
- assets/animations/
+ - assets/sounds/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see