vjg!
zIeX^2mh%I{lIrX3ryh{&)V$?L01BpG{7|&_1wji4EQ4rpKj%G6n1JSZ0tIa6=G`}KC3OP+$$L7E4mo&DBGH6;=@%qeUDie
zcwF;pWr{v|e55Dn!-=woeUJT1nPtqdW22TxO-VYLYRGxoaWJb=L%s-4Fw<=*FjA{f
z8!?rsz22j}Q$wY;&syZ=7rA8huB5_vQbnGFu4x7>LitXBVfcv3^>nT20~u<&aKSPM
zX}z$Fgob3UZ9&PMxa-;_vb2cnG>d(#(RB483l+SDGHo!VI%A#t4r!+_OlRj4{o(Y5
zql@>pZYHDb^$8(FQA=e!@sgA{X+Y}{sk6|`n{|zx=D#V~WI$FLjY;gH+UXg5Zni^>
zAloE7!b4`Q24NkNG22~Y>)eJ;%xq4R6h5#uQ=rE9D8|)J-@PiD@{XIhOSDAKaQ)`YHEk-ntWB2T%pA=J=H^v!$%!&NFxO8#oWHH
zUJ5B-Lbq~=%EOn&TWP3Xrwe!kh7(9dV`V@GR3Kj!iC
a&DMneKNAf2r?3Ai^iMajcHaL^0Q?F6DgG}2
literal 0
HcmV?d00001
diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart
index 6fe3bfa..726e91d 100644
--- a/lib/features/player/core/audiobook_player.dart
+++ b/lib/features/player/core/audiobook_player.dart
@@ -281,7 +281,7 @@ extension FormatNotificationTitle on String {
(match) {
final type = match.group(1);
return NotificationTitleType.values
- .firstWhere((element) => 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..5cc34ee
--- /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.force?.threshold ?? _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..1902187
--- /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.sleepTimerReset:
+ _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.sleepTimerReset}.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..2a158f2
--- /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'2b04fe1be82c48f4c3feb9a2c450f382f6839d05';
+
+/// 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/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart
index 4b70f64..7091b57 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,67 @@ 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(6) double threshold,
+ @Default(ShakeForce.medium) ShakeForce? force,
+ @Default(ShakeAction.sleepTimerReset) ShakeAction shakeAction,
+ @Default([ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep])
+ List 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 ShakeForce {
+ low(2.5),
+ medium(10),
+ high(15),
+ leafRustle(3),
+ breeze(7),
+ storm(12),
+ hurricane(18),
+ earthquake(25),
+ meteorShower(30),
+ supernova(40),
+ blackHole(50);
+
+ const ShakeForce(this.threshold);
+
+ final double threshold;
+
+ String get properName {
+ return name
+ .replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}')
+ .trim();
+ }
+}
+
+enum ShakeAction {
+ none,
+ playPause,
+ sleepTimerReset,
+ fastForward,
+ rewind,
+ custom,
+}
+
+enum ShakeDetectedFeedback { vibrate, beep }
diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart
index 415a60a..9f94770 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,400 @@ 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;
+ ShakeForce? get force => throw _privateConstructorUsedError;
+ ShakeAction get shakeAction => throw _privateConstructorUsedError;
+ List 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,
+ ShakeForce? force,
+ ShakeAction shakeAction,
+ List 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? force = freezed,
+ 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,
+ force: freezed == force
+ ? _value.force
+ : force // ignore: cast_nullable_to_non_nullable
+ as ShakeForce?,
+ 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 List,
+ 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,
+ ShakeForce? force,
+ ShakeAction shakeAction,
+ List 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? force = freezed,
+ 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,
+ force: freezed == force
+ ? _value.force
+ : force // ignore: cast_nullable_to_non_nullable
+ as ShakeForce?,
+ 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 List,
+ 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 = 6,
+ this.force = ShakeForce.medium,
+ this.shakeAction = ShakeAction.sleepTimerReset,
+ final List 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 ShakeForce? force;
+ @override
+ @JsonKey()
+ final ShakeAction shakeAction;
+ final List _feedback;
+ @override
+ @JsonKey()
+ List get feedback {
+ if (_feedback is EqualUnmodifiableListView) return _feedback;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_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, force: $force, 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.force, force) || other.force == force) &&
+ (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,
+ force,
+ 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 ShakeForce? force,
+ final ShakeAction shakeAction,
+ final List 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
+ ShakeForce? get force;
+ @override
+ ShakeAction get shakeAction;
+ @override
+ List 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..7b97dd0 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,81 @@ 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() ?? 6,
+ force: $enumDecodeNullable(_$ShakeForceEnumMap, json['force']) ??
+ ShakeForce.medium,
+ shakeAction:
+ $enumDecodeNullable(_$ShakeActionEnumMap, json['shakeAction']) ??
+ ShakeAction.sleepTimerReset,
+ feedback: (json['feedback'] as List?)
+ ?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e))
+ .toList() ??
+ 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,
+ 'force': _$ShakeForceEnumMap[instance.force],
+ '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 _$ShakeForceEnumMap = {
+ ShakeForce.low: 'low',
+ ShakeForce.medium: 'medium',
+ ShakeForce.high: 'high',
+ ShakeForce.leafRustle: 'leafRustle',
+ ShakeForce.breeze: 'breeze',
+ ShakeForce.storm: 'storm',
+ ShakeForce.hurricane: 'hurricane',
+ ShakeForce.earthquake: 'earthquake',
+ ShakeForce.meteorShower: 'meteorShower',
+ ShakeForce.supernova: 'supernova',
+ ShakeForce.blackHole: 'blackHole',
+};
+
+const _$ShakeActionEnumMap = {
+ ShakeAction.none: 'none',
+ ShakeAction.playPause: 'playPause',
+ ShakeAction.sleepTimerReset: 'sleepTimerReset',
+ ShakeAction.fastForward: 'fastForward',
+ ShakeAction.rewind: 'rewind',
+ ShakeAction.custom: 'custom',
+};
+
+const _$ShakeDetectedFeedbackEnumMap = {
+ ShakeDetectedFeedback.vibrate: 'vibrate',
+ ShakeDetectedFeedback.beep: 'beep',
+};
diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart
index b673add..4584d7d 100644
--- a/lib/settings/view/notification_settings_page.dart
+++ b/lib/settings/view/notification_settings_page.dart
@@ -225,7 +225,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
@@ -332,7 +332,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 +362,10 @@ class NotificationTitlePicker extends HookConsumerWidget {
children: NotificationTitleType.values
.map(
(type) => ActionChip(
- label: Text(type.stringValue),
+ label: Text(type.name),
onPressed: () {
final text = controller.text;
- final newText = '$text\$${type.stringValue}';
+ final newText = '$text\$${type.name}';
controller.text = newText;
selectedTitle.value = newText;
},
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/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