mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
feat: add shake detection functionality (#36)
* feat: add shake detection functionality and integrate vibration support * feat: add shake detector settings page
This commit is contained in:
parent
2e3b1de529
commit
b229c4f2f5
25 changed files with 1423 additions and 158 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<application
|
<application
|
||||||
android:label="Vaani"
|
android:label="Vaani"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
|
||||||
BIN
assets/sounds/beep.mp3
Normal file
BIN
assets/sounds/beep.mp3
Normal file
Binary file not shown.
|
|
@ -281,7 +281,7 @@ extension FormatNotificationTitle on String {
|
||||||
(match) {
|
(match) {
|
||||||
final type = match.group(1);
|
final type = match.group(1);
|
||||||
return NotificationTitleType.values
|
return NotificationTitleType.values
|
||||||
.firstWhere((element) => element.stringValue == type)
|
.firstWhere((element) => element.name == type)
|
||||||
.extractFrom(book) ??
|
.extractFrom(book) ??
|
||||||
match.group(0) ??
|
match.group(0) ??
|
||||||
'';
|
'';
|
||||||
|
|
|
||||||
|
|
@ -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'
|
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
|
||||||
show sleepTimerProvider;
|
show sleepTimerProvider;
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
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/extensions/inverse_lerp.dart';
|
||||||
import 'package:vaani/shared/widgets/not_implemented.dart';
|
import 'package:vaani/shared/widgets/not_implemented.dart';
|
||||||
|
|
||||||
|
|
@ -376,8 +377,8 @@ class RemainingSleepTimeDisplay extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
timer.timer == null
|
timer.timer == null
|
||||||
? timer.duration.formatSingleLargestUnit()
|
? timer.duration.smartBinaryFormat
|
||||||
: remainingTime?.formatSingleLargestUnit() ?? '',
|
: remainingTime?.smartBinaryFormat ?? '',
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
79
lib/features/shake_detection/core/shake_detector.dart
Normal file
79
lib/features/shake_detection/core/shake_detector.dart
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
138
lib/features/shake_detection/providers/shake_detector.dart
Normal file
138
lib/features/shake_detection/providers/shake_detector.dart
Normal file
|
|
@ -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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/features/shake_detection/providers/shake_detector.g.dart
Normal file
26
lib/features/shake_detection/providers/shake_detector.g.dart
Normal file
|
|
@ -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<ShakeDetector, core.ShakeDetector?>.internal(
|
||||||
|
ShakeDetector.new,
|
||||||
|
name: r'shakeDetectorProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$shakeDetectorHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$ShakeDetector = AutoDisposeNotifier<core.ShakeDetector?>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
|
|
@ -19,7 +19,7 @@ class SleepTimer {
|
||||||
|
|
||||||
set duration(Duration value) {
|
set duration(Duration value) {
|
||||||
_duration = value;
|
_duration = value;
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The player to be paused
|
/// The player to be paused
|
||||||
|
|
@ -40,7 +40,7 @@ class SleepTimer {
|
||||||
player.playbackEventStream.listen((event) {
|
player.playbackEventStream.listen((event) {
|
||||||
if (event.processingState == ProcessingState.completed ||
|
if (event.processingState == ProcessingState.completed ||
|
||||||
event.processingState == ProcessingState.idle) {
|
event.processingState == ProcessingState.idle) {
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -49,9 +49,9 @@ class SleepTimer {
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.playerStateStream.listen((state) {
|
player.playerStateStream.listen((state) {
|
||||||
if (state.playing && timer == null) {
|
if (state.playing && timer == null) {
|
||||||
startTimer();
|
startCountDown();
|
||||||
} else if (!state.playing) {
|
} else if (!state.playing) {
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -59,8 +59,8 @@ class SleepTimer {
|
||||||
_logger.fine('created with duration: $duration');
|
_logger.fine('created with duration: $duration');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// resets the timer
|
/// resets the timer and stops it
|
||||||
void reset() {
|
void clearCountDownTimer() {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer!.cancel();
|
timer!.cancel();
|
||||||
_logger.fine(
|
_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
|
/// starts the timer with the given duration or the default duration
|
||||||
void startTimer([
|
void startCountDown([
|
||||||
Duration? forDuration,
|
Duration? forDuration,
|
||||||
]) {
|
]) {
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
duration = forDuration ?? duration;
|
duration = forDuration ?? duration;
|
||||||
timer = Timer(duration, () {
|
timer = Timer(duration, () {
|
||||||
player.pause();
|
player.pause();
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
_logger.fine('paused player after $duration');
|
_logger.fine('paused player after $duration');
|
||||||
});
|
});
|
||||||
startedAt = DateTime.now();
|
startedAt = DateTime.now();
|
||||||
|
|
@ -103,7 +113,7 @@ class SleepTimer {
|
||||||
|
|
||||||
/// dispose the timer
|
/// dispose the timer
|
||||||
void dispose() {
|
void dispose() {
|
||||||
reset();
|
clearCountDownTimer();
|
||||||
for (var sub in _subscriptions) {
|
for (var sub in _subscriptions) {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'
|
import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core;
|
||||||
as core;
|
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/time_of_day.dart';
|
import 'package:vaani/shared/extensions/time_of_day.dart';
|
||||||
|
|
||||||
|
|
@ -48,9 +47,16 @@ class SleepTimer extends _$SleepTimer {
|
||||||
);
|
);
|
||||||
ref.onDispose(timer.dispose);
|
ref.onDispose(timer.dispose);
|
||||||
state = timer;
|
state = timer;
|
||||||
|
state!.startCountDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void restartTimer() {
|
||||||
|
state?.restartTimer();
|
||||||
|
|
||||||
|
ref.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void cancelTimer() {
|
void cancelTimer() {
|
||||||
state?.dispose();
|
state?.dispose();
|
||||||
state = null;
|
state = null;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$sleepTimerHash() => r'ad77e82c1b513bbc62815c64ce1ed403f92fc055';
|
String _$sleepTimerHash() => r'9d9f20267da91e5483151b58b7d4d7c0762c3ca7';
|
||||||
|
|
||||||
/// See also [SleepTimer].
|
/// See also [SleepTimer].
|
||||||
@ProviderFor(SleepTimer)
|
@ProviderFor(SleepTimer)
|
||||||
|
|
|
||||||
|
|
@ -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/playback_reporting/providers/playback_reporter_provider.dart';
|
||||||
import 'package:vaani/features/player/core/init.dart';
|
import 'package:vaani/features/player/core/init.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.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/features/sleep_timer/providers/sleep_timer_provider.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
|
|
@ -93,6 +94,7 @@ class _EagerInitialization extends ConsumerWidget {
|
||||||
ref.watch(sleepTimerProvider);
|
ref.watch(sleepTimerProvider);
|
||||||
ref.watch(playbackReporterProvider);
|
ref.watch(playbackReporterProvider);
|
||||||
ref.watch(simpleDownloadManagerProvider);
|
ref.watch(simpleDownloadManagerProvider);
|
||||||
|
ref.watch(shakeDetectorProvider);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
|
debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@ class Routes {
|
||||||
name: 'playerSettings',
|
name: 'playerSettings',
|
||||||
parentRoute: settings,
|
parentRoute: settings,
|
||||||
);
|
);
|
||||||
|
static const shakeDetectorSettings = _SimpleRoute(
|
||||||
|
pathName: 'shakeDetector',
|
||||||
|
name: 'shakeDetectorSettings',
|
||||||
|
parentRoute: settings,
|
||||||
|
);
|
||||||
|
|
||||||
// search and explore
|
// search and explore
|
||||||
static const search = _SimpleRoute(
|
static const search = _SimpleRoute(
|
||||||
|
|
|
||||||
|
|
@ -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/auto_sleep_timer_settings_page.dart';
|
||||||
import 'package:vaani/settings/view/notification_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/player_settings_page.dart';
|
||||||
|
import 'package:vaani/settings/view/shake_detector_settings_page.dart';
|
||||||
|
|
||||||
import 'scaffold_with_nav_bar.dart';
|
import 'scaffold_with_nav_bar.dart';
|
||||||
import 'transitions/slide.dart';
|
import 'transitions/slide.dart';
|
||||||
|
|
@ -195,6 +196,13 @@ class MyAppRouter {
|
||||||
pageBuilder:
|
pageBuilder:
|
||||||
defaultPageBuilder(const PlayerSettingsPage()),
|
defaultPageBuilder(const PlayerSettingsPage()),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.shakeDetectorSettings.pathName,
|
||||||
|
name: Routes.shakeDetectorSettings.name,
|
||||||
|
pageBuilder: defaultPageBuilder(
|
||||||
|
const ShakeDetectorSettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ class AppSettings with _$AppSettings {
|
||||||
@Default(PlayerSettings()) PlayerSettings playerSettings,
|
@Default(PlayerSettings()) PlayerSettings playerSettings,
|
||||||
@Default(DownloadSettings()) DownloadSettings downloadSettings,
|
@Default(DownloadSettings()) DownloadSettings downloadSettings,
|
||||||
@Default(NotificationSettings()) NotificationSettings notificationSettings,
|
@Default(NotificationSettings()) NotificationSettings notificationSettings,
|
||||||
|
@Default(ShakeDetectionSettings())
|
||||||
|
ShakeDetectionSettings shakeDetectionSettings,
|
||||||
}) = _AppSettings;
|
}) = _AppSettings;
|
||||||
|
|
||||||
factory AppSettings.fromJson(Map<String, dynamic> json) =>
|
factory AppSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|
@ -165,17 +167,13 @@ class NotificationSettings with _$NotificationSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationTitleType {
|
enum NotificationTitleType {
|
||||||
chapterTitle('chapterTitle'),
|
chapterTitle,
|
||||||
bookTitle('bookTitle'),
|
bookTitle,
|
||||||
author('author'),
|
author,
|
||||||
subtitle('subtitle'),
|
subtitle,
|
||||||
series('series'),
|
series,
|
||||||
narrator('narrator'),
|
narrator,
|
||||||
year('year');
|
year,
|
||||||
|
|
||||||
const NotificationTitleType(this.stringValue);
|
|
||||||
|
|
||||||
final String stringValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationMediaControl {
|
enum NotificationMediaControl {
|
||||||
|
|
@ -190,3 +188,41 @@ enum NotificationMediaControl {
|
||||||
|
|
||||||
final IconData icon;
|
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<ShakeDetectedFeedback> 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<String, dynamic> json) =>
|
||||||
|
_$ShakeDetectionSettingsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShakeDirection { horizontal, vertical }
|
||||||
|
|
||||||
|
enum ShakeAction {
|
||||||
|
none,
|
||||||
|
playPause,
|
||||||
|
resetSleepTimer,
|
||||||
|
fastForward,
|
||||||
|
rewind,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShakeDetectedFeedback { vibrate, beep }
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ mixin _$AppSettings {
|
||||||
DownloadSettings get downloadSettings => throw _privateConstructorUsedError;
|
DownloadSettings get downloadSettings => throw _privateConstructorUsedError;
|
||||||
NotificationSettings get notificationSettings =>
|
NotificationSettings get notificationSettings =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
ShakeDetectionSettings get shakeDetectionSettings =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this AppSettings to a JSON map.
|
/// Serializes this AppSettings to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
@ -46,12 +48,14 @@ abstract class $AppSettingsCopyWith<$Res> {
|
||||||
{ThemeSettings themeSettings,
|
{ThemeSettings themeSettings,
|
||||||
PlayerSettings playerSettings,
|
PlayerSettings playerSettings,
|
||||||
DownloadSettings downloadSettings,
|
DownloadSettings downloadSettings,
|
||||||
NotificationSettings notificationSettings});
|
NotificationSettings notificationSettings,
|
||||||
|
ShakeDetectionSettings shakeDetectionSettings});
|
||||||
|
|
||||||
$ThemeSettingsCopyWith<$Res> get themeSettings;
|
$ThemeSettingsCopyWith<$Res> get themeSettings;
|
||||||
$PlayerSettingsCopyWith<$Res> get playerSettings;
|
$PlayerSettingsCopyWith<$Res> get playerSettings;
|
||||||
$DownloadSettingsCopyWith<$Res> get downloadSettings;
|
$DownloadSettingsCopyWith<$Res> get downloadSettings;
|
||||||
$NotificationSettingsCopyWith<$Res> get notificationSettings;
|
$NotificationSettingsCopyWith<$Res> get notificationSettings;
|
||||||
|
$ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -73,6 +77,7 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||||
Object? playerSettings = null,
|
Object? playerSettings = null,
|
||||||
Object? downloadSettings = null,
|
Object? downloadSettings = null,
|
||||||
Object? notificationSettings = null,
|
Object? notificationSettings = null,
|
||||||
|
Object? shakeDetectionSettings = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
themeSettings: null == themeSettings
|
themeSettings: null == themeSettings
|
||||||
|
|
@ -91,6 +96,10 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||||
? _value.notificationSettings
|
? _value.notificationSettings
|
||||||
: notificationSettings // ignore: cast_nullable_to_non_nullable
|
: notificationSettings // ignore: cast_nullable_to_non_nullable
|
||||||
as NotificationSettings,
|
as NotificationSettings,
|
||||||
|
shakeDetectionSettings: null == shakeDetectionSettings
|
||||||
|
? _value.shakeDetectionSettings
|
||||||
|
: shakeDetectionSettings // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ShakeDetectionSettings,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +143,17 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings>
|
||||||
return _then(_value.copyWith(notificationSettings: value) as $Val);
|
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
|
/// @nodoc
|
||||||
|
|
@ -148,7 +168,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res>
|
||||||
{ThemeSettings themeSettings,
|
{ThemeSettings themeSettings,
|
||||||
PlayerSettings playerSettings,
|
PlayerSettings playerSettings,
|
||||||
DownloadSettings downloadSettings,
|
DownloadSettings downloadSettings,
|
||||||
NotificationSettings notificationSettings});
|
NotificationSettings notificationSettings,
|
||||||
|
ShakeDetectionSettings shakeDetectionSettings});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$ThemeSettingsCopyWith<$Res> get themeSettings;
|
$ThemeSettingsCopyWith<$Res> get themeSettings;
|
||||||
|
|
@ -158,6 +179,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res>
|
||||||
$DownloadSettingsCopyWith<$Res> get downloadSettings;
|
$DownloadSettingsCopyWith<$Res> get downloadSettings;
|
||||||
@override
|
@override
|
||||||
$NotificationSettingsCopyWith<$Res> get notificationSettings;
|
$NotificationSettingsCopyWith<$Res> get notificationSettings;
|
||||||
|
@override
|
||||||
|
$ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -177,6 +200,7 @@ class __$$AppSettingsImplCopyWithImpl<$Res>
|
||||||
Object? playerSettings = null,
|
Object? playerSettings = null,
|
||||||
Object? downloadSettings = null,
|
Object? downloadSettings = null,
|
||||||
Object? notificationSettings = null,
|
Object? notificationSettings = null,
|
||||||
|
Object? shakeDetectionSettings = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$AppSettingsImpl(
|
return _then(_$AppSettingsImpl(
|
||||||
themeSettings: null == themeSettings
|
themeSettings: null == themeSettings
|
||||||
|
|
@ -195,6 +219,10 @@ class __$$AppSettingsImplCopyWithImpl<$Res>
|
||||||
? _value.notificationSettings
|
? _value.notificationSettings
|
||||||
: notificationSettings // ignore: cast_nullable_to_non_nullable
|
: notificationSettings // ignore: cast_nullable_to_non_nullable
|
||||||
as NotificationSettings,
|
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.themeSettings = const ThemeSettings(),
|
||||||
this.playerSettings = const PlayerSettings(),
|
this.playerSettings = const PlayerSettings(),
|
||||||
this.downloadSettings = const DownloadSettings(),
|
this.downloadSettings = const DownloadSettings(),
|
||||||
this.notificationSettings = const NotificationSettings()});
|
this.notificationSettings = const NotificationSettings(),
|
||||||
|
this.shakeDetectionSettings = const ShakeDetectionSettings()});
|
||||||
|
|
||||||
factory _$AppSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$AppSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$AppSettingsImplFromJson(json);
|
_$$AppSettingsImplFromJson(json);
|
||||||
|
|
@ -223,10 +252,13 @@ class _$AppSettingsImpl implements _AppSettings {
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final NotificationSettings notificationSettings;
|
final NotificationSettings notificationSettings;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final ShakeDetectionSettings shakeDetectionSettings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
|
|
@ -241,13 +273,15 @@ class _$AppSettingsImpl implements _AppSettings {
|
||||||
(identical(other.downloadSettings, downloadSettings) ||
|
(identical(other.downloadSettings, downloadSettings) ||
|
||||||
other.downloadSettings == downloadSettings) &&
|
other.downloadSettings == downloadSettings) &&
|
||||||
(identical(other.notificationSettings, notificationSettings) ||
|
(identical(other.notificationSettings, notificationSettings) ||
|
||||||
other.notificationSettings == notificationSettings));
|
other.notificationSettings == notificationSettings) &&
|
||||||
|
(identical(other.shakeDetectionSettings, shakeDetectionSettings) ||
|
||||||
|
other.shakeDetectionSettings == shakeDetectionSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, themeSettings, playerSettings,
|
int get hashCode => Object.hash(runtimeType, themeSettings, playerSettings,
|
||||||
downloadSettings, notificationSettings);
|
downloadSettings, notificationSettings, shakeDetectionSettings);
|
||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -270,7 +304,8 @@ abstract class _AppSettings implements AppSettings {
|
||||||
{final ThemeSettings themeSettings,
|
{final ThemeSettings themeSettings,
|
||||||
final PlayerSettings playerSettings,
|
final PlayerSettings playerSettings,
|
||||||
final DownloadSettings downloadSettings,
|
final DownloadSettings downloadSettings,
|
||||||
final NotificationSettings notificationSettings}) = _$AppSettingsImpl;
|
final NotificationSettings notificationSettings,
|
||||||
|
final ShakeDetectionSettings shakeDetectionSettings}) = _$AppSettingsImpl;
|
||||||
|
|
||||||
factory _AppSettings.fromJson(Map<String, dynamic> json) =
|
factory _AppSettings.fromJson(Map<String, dynamic> json) =
|
||||||
_$AppSettingsImpl.fromJson;
|
_$AppSettingsImpl.fromJson;
|
||||||
|
|
@ -283,6 +318,8 @@ abstract class _AppSettings implements AppSettings {
|
||||||
DownloadSettings get downloadSettings;
|
DownloadSettings get downloadSettings;
|
||||||
@override
|
@override
|
||||||
NotificationSettings get notificationSettings;
|
NotificationSettings get notificationSettings;
|
||||||
|
@override
|
||||||
|
ShakeDetectionSettings get shakeDetectionSettings;
|
||||||
|
|
||||||
/// Create a copy of AppSettings
|
/// Create a copy of AppSettings
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -2381,3 +2418,377 @@ abstract class _NotificationSettings implements NotificationSettings {
|
||||||
_$$NotificationSettingsImplCopyWith<_$NotificationSettingsImpl>
|
_$$NotificationSettingsImplCopyWith<_$NotificationSettingsImpl>
|
||||||
get copyWith => throw _privateConstructorUsedError;
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShakeDetectionSettings _$ShakeDetectionSettingsFromJson(
|
||||||
|
Map<String, dynamic> 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<ShakeDetectedFeedback> 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<String, dynamic> 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<ShakeDetectionSettings> 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<ShakeDetectedFeedback> 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<ShakeDetectedFeedback>,
|
||||||
|
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<ShakeDetectedFeedback> 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<ShakeDetectedFeedback>,
|
||||||
|
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<ShakeDetectedFeedback> 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<String, dynamic> 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<ShakeDetectedFeedback> _feedback;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
Set<ShakeDetectedFeedback> 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<String, dynamic> 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<ShakeDetectedFeedback> feedback,
|
||||||
|
final double beepVolume,
|
||||||
|
final Duration shakeTriggerCoolDown,
|
||||||
|
final int shakeTriggerCount,
|
||||||
|
final Duration samplingPeriod}) = _$ShakeDetectionSettingsImpl;
|
||||||
|
|
||||||
|
factory _ShakeDetectionSettings.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$ShakeDetectionSettingsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isEnabled;
|
||||||
|
@override
|
||||||
|
ShakeDirection get direction;
|
||||||
|
@override
|
||||||
|
double get threshold;
|
||||||
|
@override
|
||||||
|
ShakeAction get shakeAction;
|
||||||
|
@override
|
||||||
|
Set<ShakeDetectedFeedback> 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ _$AppSettingsImpl _$$AppSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||||
? const NotificationSettings()
|
? const NotificationSettings()
|
||||||
: NotificationSettings.fromJson(
|
: NotificationSettings.fromJson(
|
||||||
json['notificationSettings'] as Map<String, dynamic>),
|
json['notificationSettings'] as Map<String, dynamic>),
|
||||||
|
shakeDetectionSettings: json['shakeDetectionSettings'] == null
|
||||||
|
? const ShakeDetectionSettings()
|
||||||
|
: ShakeDetectionSettings.fromJson(
|
||||||
|
json['shakeDetectionSettings'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
|
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
|
||||||
|
|
@ -32,6 +36,7 @@ Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
|
||||||
'playerSettings': instance.playerSettings,
|
'playerSettings': instance.playerSettings,
|
||||||
'downloadSettings': instance.downloadSettings,
|
'downloadSettings': instance.downloadSettings,
|
||||||
'notificationSettings': instance.notificationSettings,
|
'notificationSettings': instance.notificationSettings,
|
||||||
|
'shakeDetectionSettings': instance.shakeDetectionSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map<String, dynamic> json) =>
|
_$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
|
@ -274,3 +279,63 @@ const _$NotificationMediaControlEnumMap = {
|
||||||
NotificationMediaControl.skipToNextChapter: 'skipToNextChapter',
|
NotificationMediaControl.skipToNextChapter: 'skipToNextChapter',
|
||||||
NotificationMediaControl.skipToPreviousChapter: 'skipToPreviousChapter',
|
NotificationMediaControl.skipToPreviousChapter: 'skipToPreviousChapter',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$ShakeDetectionSettingsImpl _$$ShakeDetectionSettingsImplFromJson(
|
||||||
|
Map<String, dynamic> 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<dynamic>?)
|
||||||
|
?.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<String, dynamic> _$$ShakeDetectionSettingsImplToJson(
|
||||||
|
_$ShakeDetectionSettingsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'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',
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/app_settings.dart' as model;
|
import 'package:vaani/settings/models/app_settings.dart' as model;
|
||||||
import 'package:vaani/settings/view/simple_settings_page.dart';
|
import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
|
import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart';
|
||||||
|
|
||||||
class AppSettingsPage extends HookConsumerWidget {
|
class AppSettingsPage extends HookConsumerWidget {
|
||||||
const AppSettingsPage({
|
const AppSettingsPage({
|
||||||
|
|
@ -30,39 +31,6 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
return SimpleSettingsPage(
|
return SimpleSettingsPage(
|
||||||
title: const Text('App Settings'),
|
title: const Text('App Settings'),
|
||||||
sections: [
|
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
|
// Appearance section
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
margin: const EdgeInsetsDirectional.symmetric(
|
margin: const EdgeInsetsDirectional.symmetric(
|
||||||
|
|
@ -106,20 +74,19 @@ class AppSettingsPage extends HookConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Sleep Timer section
|
// General section
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
margin: const EdgeInsetsDirectional.symmetric(
|
margin: const EdgeInsetsDirectional.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
vertical: 8.0,
|
vertical: 8.0,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Sleep Timer',
|
'General',
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
tiles: [
|
tiles: [
|
||||||
SettingsTile.navigation(
|
NavigationWithSwitchTile(
|
||||||
// initialValue: sleepTimerSettings.autoTurnOnTimer,
|
title: const Text('Auto Turn On Sleep Timer'),
|
||||||
title: const Text('Auto Turn On Timer'),
|
|
||||||
description: const Text(
|
description: const Text(
|
||||||
'Automatically turn on the sleep timer based on the time of day',
|
'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)
|
||||||
: const Icon(Icons.timer_off),
|
: const Icon(Icons.timer_off),
|
||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
// push the sleep timer settings page
|
|
||||||
context.pushNamed(Routes.autoSleepTimerSettings.name);
|
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,
|
value: sleepTimerSettings.autoTurnOnTimer,
|
||||||
onChanged: (value) {
|
onToggle: (value) {
|
||||||
ref.read(appSettingsProvider.notifier).update(
|
ref.read(appSettingsProvider.notifier).update(
|
||||||
appSettings.copyWith.playerSettings
|
appSettings.copyWith.playerSettings.sleepTimerSettings(
|
||||||
.sleepTimerSettings(
|
|
||||||
autoTurnOnTimer: value,
|
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
|
// Backup and Restore section
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
margin: const EdgeInsetsDirectional.symmetric(
|
margin: const EdgeInsetsDirectional.symmetric(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/app_settings.dart';
|
import 'package:vaani/settings/models/app_settings.dart';
|
||||||
import 'package:vaani/settings/view/buttons.dart';
|
import 'package:vaani/settings/view/buttons.dart';
|
||||||
import 'package:vaani/settings/view/simple_settings_page.dart';
|
import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
|
import 'package:vaani/shared/extensions/enum.dart';
|
||||||
|
|
||||||
class NotificationSettingsPage extends HookConsumerWidget {
|
class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
const NotificationSettingsPage({
|
const NotificationSettingsPage({
|
||||||
|
|
@ -16,7 +17,7 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettings = ref.watch(appSettingsProvider);
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
final notificationSettings = appSettings.notificationSettings;
|
final notificationSettings = appSettings.notificationSettings;
|
||||||
|
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||||
return SimpleSettingsPage(
|
return SimpleSettingsPage(
|
||||||
title: const Text('Notification Settings'),
|
title: const Text('Notification Settings'),
|
||||||
sections: [
|
sections: [
|
||||||
|
|
@ -37,7 +38,10 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: notificationSettings.primaryTitle,
|
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: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: notificationSettings.secondaryTitle,
|
text: notificationSettings.secondaryTitle,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -151,20 +158,18 @@ class NotificationSettingsPage extends HookConsumerWidget {
|
||||||
title: const Text('Media Controls'),
|
title: const Text('Media Controls'),
|
||||||
leading: const Icon(Icons.control_camera),
|
leading: const Icon(Icons.control_camera),
|
||||||
// description: const Text('Select the media controls to display'),
|
// description: const Text('Select the media controls to display'),
|
||||||
description: Column(
|
description: const Text('Select the media controls to display'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
trailing: Wrap(
|
||||||
children: [
|
|
||||||
const Text('Select the media controls to display'),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: notificationSettings.mediaControls
|
children: notificationSettings.mediaControls
|
||||||
.map(
|
.map(
|
||||||
(control) => Icon(control.icon),
|
(control) => Icon(
|
||||||
|
control.icon,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
final selectedControls =
|
final selectedControls =
|
||||||
await showDialog<List<NotificationMediaControl>>(
|
await showDialog<List<NotificationMediaControl>>(
|
||||||
|
|
@ -225,7 +230,7 @@ class MediaControlsPicker extends HookConsumerWidget {
|
||||||
OkButton(
|
OkButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(selectedMediaControls.value);
|
Navigator.of(context).pop(selectedMediaControls.value);
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// a list of chips to easily select the media controls to display
|
// a list of chips to easily select the media controls to display
|
||||||
|
|
@ -235,14 +240,8 @@ class MediaControlsPicker extends HookConsumerWidget {
|
||||||
children: NotificationMediaControl.values
|
children: NotificationMediaControl.values
|
||||||
.map(
|
.map(
|
||||||
(control) => ChoiceChip(
|
(control) => ChoiceChip(
|
||||||
label: Row(
|
avatar: Icon(control.icon),
|
||||||
mainAxisSize: MainAxisSize.min,
|
label: Text(control.pascalCase),
|
||||||
children: [
|
|
||||||
Icon(control.icon),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Text(control.name),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
selected: selectedMediaControls.value.contains(control),
|
selected: selectedMediaControls.value.contains(control),
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
|
|
@ -332,7 +331,7 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
||||||
OkButton(
|
OkButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(selectedTitle.value);
|
Navigator.of(context).pop(selectedTitle.value);
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// a list of chips to easily insert available fields into the text field
|
// a list of chips to easily insert available fields into the text field
|
||||||
|
|
@ -362,10 +361,10 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
||||||
children: NotificationTitleType.values
|
children: NotificationTitleType.values
|
||||||
.map(
|
.map(
|
||||||
(type) => ActionChip(
|
(type) => ActionChip(
|
||||||
label: Text(type.stringValue),
|
label: Text(type.pascalCase),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final text = controller.text;
|
final text = controller.text;
|
||||||
final newText = '$text\$${type.stringValue}';
|
final newText = '$text\$${type.name}';
|
||||||
controller.text = newText;
|
controller.text = newText;
|
||||||
selectedTitle.value = newText;
|
selectedTitle.value = newText;
|
||||||
},
|
},
|
||||||
|
|
@ -378,16 +377,3 @@ class NotificationTitlePicker extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> showNotificationTitlePicker(
|
|
||||||
BuildContext context, {
|
|
||||||
required String initialValue,
|
|
||||||
required String title,
|
|
||||||
}) async {
|
|
||||||
return showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return NotificationTitlePicker(initialValue: initialValue, title: title);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettings = ref.watch(appSettingsProvider);
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
final playerSettings = appSettings.playerSettings;
|
final playerSettings = appSettings.playerSettings;
|
||||||
|
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||||
|
|
||||||
return SimpleSettingsPage(
|
return SimpleSettingsPage(
|
||||||
title: const Text('Player Settings'),
|
title: const Text('Player Settings'),
|
||||||
|
|
@ -47,7 +48,11 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
// preferred default speed
|
// preferred default speed
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: const Text('Default Speed'),
|
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),
|
leading: const Icon(Icons.speed),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
final newSpeed = await showDialog(
|
final newSpeed = await showDialog(
|
||||||
|
|
@ -70,6 +75,8 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
title: const Text('Speed Options'),
|
title: const Text('Speed Options'),
|
||||||
description: Text(
|
description: Text(
|
||||||
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
|
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
|
||||||
),
|
),
|
||||||
leading: const Icon(Icons.speed),
|
leading: const Icon(Icons.speed),
|
||||||
onPressed: (context) async {
|
onPressed: (context) async {
|
||||||
|
|
@ -104,7 +111,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.minimumPositionForReporting.smartBinaryFormat,
|
.minimumPositionForReporting.smartBinaryFormat,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: ' of the book'),
|
const TextSpan(text: ' of the book'),
|
||||||
],
|
],
|
||||||
|
|
@ -141,7 +151,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
.markCompleteWhenTimeLeft.smartBinaryFormat,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: ' left in the book'),
|
const TextSpan(text: ' left in the book'),
|
||||||
],
|
],
|
||||||
|
|
@ -178,7 +191,10 @@ class PlayerSettingsPage extends HookConsumerWidget {
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: playerSettings
|
text: playerSettings
|
||||||
.playbackReportInterval.smartBinaryFormat,
|
.playbackReportInterval.smartBinaryFormat,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: ' to the server'),
|
const TextSpan(text: ' to the server'),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
395
lib/settings/view/shake_detector_settings_page.dart
Normal file
395
lib/settings/view/shake_detector_settings_page.dart
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
|
import 'package:vaani/settings/models/app_settings.dart';
|
||||||
|
import 'package:vaani/settings/view/buttons.dart';
|
||||||
|
import 'package:vaani/settings/view/simple_settings_page.dart';
|
||||||
|
import 'package:vaani/shared/extensions/enum.dart';
|
||||||
|
|
||||||
|
class ShakeDetectorSettingsPage extends HookConsumerWidget {
|
||||||
|
const ShakeDetectorSettingsPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final appSettings = ref.watch(appSettingsProvider);
|
||||||
|
final shakeDetectionSettings = appSettings.shakeDetectionSettings;
|
||||||
|
final isShakeDetectionEnabled = shakeDetectionSettings.isEnabled;
|
||||||
|
final selectedValueColor = isShakeDetectionEnabled
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).disabledColor;
|
||||||
|
|
||||||
|
return SimpleSettingsPage(
|
||||||
|
title: const Text('Shake Detector Settings'),
|
||||||
|
sections: [
|
||||||
|
SettingsSection(
|
||||||
|
margin: const EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
tiles: [
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
leading: shakeDetectionSettings.isEnabled
|
||||||
|
? const Icon(Icons.vibration)
|
||||||
|
: const Icon(Icons.not_interested),
|
||||||
|
title: const Text('Enable Shake Detection'),
|
||||||
|
description: const Text(
|
||||||
|
'Enable shake detection to do various actions',
|
||||||
|
),
|
||||||
|
initialValue: shakeDetectionSettings.isEnabled,
|
||||||
|
onToggle: (value) {
|
||||||
|
ref.read(appSettingsProvider.notifier).update(
|
||||||
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
|
isEnabled: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Shake Detection Settings
|
||||||
|
SettingsSection(
|
||||||
|
tiles: [
|
||||||
|
SettingsTile(
|
||||||
|
enabled: isShakeDetectionEnabled,
|
||||||
|
leading: const Icon(Icons.flag_circle),
|
||||||
|
title: const Text('Shake Activation Threshold'),
|
||||||
|
description: const Text(
|
||||||
|
'The higher the threshold, the harder you need to shake',
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
'${shakeDetectionSettings.threshold} m/s²',
|
||||||
|
style: TextStyle(
|
||||||
|
color: selectedValueColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: (context) async {
|
||||||
|
final newThreshold = await showDialog<double>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShakeForceSelector(
|
||||||
|
initialValue: shakeDetectionSettings.threshold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newThreshold != null) {
|
||||||
|
ref.read(appSettingsProvider.notifier).update(
|
||||||
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
|
threshold: newThreshold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// shake action
|
||||||
|
SettingsTile(
|
||||||
|
enabled: isShakeDetectionEnabled,
|
||||||
|
leading: const Icon(Icons.directions_run),
|
||||||
|
title: const Text('Shake Action'),
|
||||||
|
description: const Text(
|
||||||
|
'The action to perform when a shake is detected',
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
shakeDetectionSettings.shakeAction.icon,
|
||||||
|
color: selectedValueColor,
|
||||||
|
),
|
||||||
|
onPressed: (context) async {
|
||||||
|
final newShakeAction = await showDialog<ShakeAction>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShakeActionSelector(
|
||||||
|
initialValue: shakeDetectionSettings.shakeAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newShakeAction != null) {
|
||||||
|
ref.read(appSettingsProvider.notifier).update(
|
||||||
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
|
shakeAction: newShakeAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// shake feedback
|
||||||
|
SettingsTile(
|
||||||
|
enabled: isShakeDetectionEnabled,
|
||||||
|
leading: const Icon(Icons.feedback),
|
||||||
|
title: const Text('Shake Feedback'),
|
||||||
|
description: const Text(
|
||||||
|
'The feedback to give when a shake is detected',
|
||||||
|
),
|
||||||
|
trailing: shakeDetectionSettings.feedback.isEmpty
|
||||||
|
? Icon(
|
||||||
|
Icons.not_interested,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
)
|
||||||
|
: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
children: shakeDetectionSettings.feedback.map(
|
||||||
|
(feedback) {
|
||||||
|
return Icon(
|
||||||
|
feedback.icon,
|
||||||
|
color: selectedValueColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
onPressed: (context) async {
|
||||||
|
final newFeedback =
|
||||||
|
await showDialog<Set<ShakeDetectedFeedback>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShakeFeedbackSelector(
|
||||||
|
initialValue: shakeDetectionSettings.feedback,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newFeedback != null) {
|
||||||
|
ref.read(appSettingsProvider.notifier).update(
|
||||||
|
appSettings.copyWith.shakeDetectionSettings(
|
||||||
|
feedback: newFeedback,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShakeFeedbackSelector extends HookConsumerWidget {
|
||||||
|
const ShakeFeedbackSelector({
|
||||||
|
super.key,
|
||||||
|
this.initialValue = const {ShakeDetectedFeedback.vibrate},
|
||||||
|
});
|
||||||
|
|
||||||
|
final Set<ShakeDetectedFeedback> initialValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final feedback = useState(initialValue);
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select Shake Feedback'),
|
||||||
|
content: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: ShakeDetectedFeedback.values
|
||||||
|
.map(
|
||||||
|
(feedbackType) => ChoiceChip(
|
||||||
|
avatar: Icon(feedbackType.icon),
|
||||||
|
label: Text(feedbackType.pascalCase),
|
||||||
|
tooltip: feedbackType.description,
|
||||||
|
onSelected: (val) {
|
||||||
|
if (feedback.value.contains(feedbackType)) {
|
||||||
|
feedback.value = feedback.value
|
||||||
|
.where((element) => element != feedbackType)
|
||||||
|
.toSet();
|
||||||
|
} else {
|
||||||
|
feedback.value = {...feedback.value, feedbackType};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected: feedback.value.contains(feedbackType),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
const CancelButton(),
|
||||||
|
OkButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(feedback.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShakeActionSelector extends HookConsumerWidget {
|
||||||
|
const ShakeActionSelector({
|
||||||
|
super.key,
|
||||||
|
this.initialValue = ShakeAction.resetSleepTimer,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ShakeAction initialValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final shakeAction = useState(initialValue);
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select Shake Action'),
|
||||||
|
content: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: ShakeAction.values
|
||||||
|
.map(
|
||||||
|
// chips with radio buttons as one of the options can be selected
|
||||||
|
(action) => ChoiceChip(
|
||||||
|
avatar: Icon(action.icon),
|
||||||
|
label: Text(action.pascalCase),
|
||||||
|
onSelected: (val) {
|
||||||
|
shakeAction.value = action;
|
||||||
|
},
|
||||||
|
selected: shakeAction.value == action,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
const CancelButton(),
|
||||||
|
OkButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(shakeAction.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShakeForceSelector extends HookConsumerWidget {
|
||||||
|
const ShakeForceSelector({
|
||||||
|
super.key,
|
||||||
|
this.initialValue = 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double initialValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final shakeForce = useState(initialValue);
|
||||||
|
final controller = useTextEditingController(text: initialValue.toString());
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select Shake Activation Threshold'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: controller,
|
||||||
|
onChanged: (value) {
|
||||||
|
final newThreshold = double.tryParse(value);
|
||||||
|
if (newThreshold != null) {
|
||||||
|
shakeForce.value = newThreshold;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
// clear button
|
||||||
|
suffix: IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
controller.clear();
|
||||||
|
shakeForce.value = 0;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
helper: const Text(
|
||||||
|
'Enter a number to set the threshold in m/s²',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: ShakeForce.values
|
||||||
|
.map(
|
||||||
|
(force) => ChoiceChip(
|
||||||
|
label: Text(force.pascalCase),
|
||||||
|
onSelected: (val) {
|
||||||
|
controller.text = force.threshold.toString();
|
||||||
|
shakeForce.value = force.threshold;
|
||||||
|
},
|
||||||
|
selected: shakeForce.value == force.threshold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
const CancelButton(),
|
||||||
|
OkButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(shakeForce.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShakeForce {
|
||||||
|
whisper(0.5),
|
||||||
|
low(2.5),
|
||||||
|
medium(5),
|
||||||
|
high(7.5),
|
||||||
|
storm(10),
|
||||||
|
hurricane(15),
|
||||||
|
earthquake(20),
|
||||||
|
meteorShower(30),
|
||||||
|
supernova(40),
|
||||||
|
blackHole(50);
|
||||||
|
|
||||||
|
const ShakeForce(this.threshold);
|
||||||
|
|
||||||
|
final double threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShakeActionIcon on ShakeAction {
|
||||||
|
IconData? get icon {
|
||||||
|
switch (this) {
|
||||||
|
case ShakeAction.none:
|
||||||
|
return Icons.not_interested;
|
||||||
|
case ShakeAction.resetSleepTimer:
|
||||||
|
return Icons.timer;
|
||||||
|
case ShakeAction.playPause:
|
||||||
|
return Icons.play_arrow;
|
||||||
|
// case ShakeAction.nextChapter:
|
||||||
|
// return Icons.skip_next;
|
||||||
|
// case ShakeAction.previousChapter:
|
||||||
|
// return Icons.skip_previous;
|
||||||
|
// case ShakeAction.volumeUp:
|
||||||
|
// return Icons.volume_up;
|
||||||
|
// case ShakeAction.volumeDown:
|
||||||
|
// return Icons.volume_down;
|
||||||
|
case ShakeAction.fastForward:
|
||||||
|
return Icons.fast_forward;
|
||||||
|
case ShakeAction.rewind:
|
||||||
|
return Icons.fast_rewind;
|
||||||
|
default:
|
||||||
|
return Icons.question_mark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on ShakeDetectedFeedback {
|
||||||
|
IconData? get icon {
|
||||||
|
switch (this) {
|
||||||
|
case ShakeDetectedFeedback.vibrate:
|
||||||
|
return Icons.vibration;
|
||||||
|
case ShakeDetectedFeedback.beep:
|
||||||
|
return Icons.volume_up;
|
||||||
|
default:
|
||||||
|
return Icons.question_mark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get description {
|
||||||
|
switch (this) {
|
||||||
|
case ShakeDetectedFeedback.vibrate:
|
||||||
|
return 'Vibrate the device';
|
||||||
|
case ShakeDetectedFeedback.beep:
|
||||||
|
return 'Play a beep sound';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/settings/view/widgets/navigation_with_switch_tile.dart
Normal file
56
lib/settings/view/widgets/navigation_with_switch_tile.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
|
||||||
|
|
||||||
|
class NavigationWithSwitchTile extends AbstractSettingsTile {
|
||||||
|
const NavigationWithSwitchTile({
|
||||||
|
this.leading,
|
||||||
|
// this.trailing,
|
||||||
|
required this.value,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.descriptionInlineIos = false,
|
||||||
|
this.onPressed,
|
||||||
|
this.enabled = true,
|
||||||
|
this.backgroundColor,
|
||||||
|
super.key,
|
||||||
|
this.onToggle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final Widget? description;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final bool descriptionInlineIos;
|
||||||
|
final bool enabled;
|
||||||
|
final Widget? leading;
|
||||||
|
final Function(BuildContext)? onPressed;
|
||||||
|
final bool value;
|
||||||
|
final Function(bool)? onToggle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SettingsTile.navigation(
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
descriptionInlineIos: descriptionInlineIos,
|
||||||
|
enabled: enabled,
|
||||||
|
leading: leading,
|
||||||
|
onPressed: onPressed,
|
||||||
|
trailing: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
VerticalDivider(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||||
|
indent: 8.0,
|
||||||
|
endIndent: 8.0,
|
||||||
|
),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: value,
|
||||||
|
onChanged: onToggle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,16 @@ extension DurationFormat on Duration {
|
||||||
return '${seconds}s';
|
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 {
|
extension OnlyTime on DateTime {
|
||||||
|
|
|
||||||
25
lib/shared/extensions/enum.dart
Normal file
25
lib/shared/extensions/enum.dart
Normal file
|
|
@ -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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
|
|
@ -1363,6 +1363,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ dependencies:
|
||||||
path: ./shelfsdk
|
path: ./shelfsdk
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
url_launcher: ^6.2.6
|
url_launcher: ^6.2.6
|
||||||
|
vibration: ^2.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.9
|
build_runner: ^2.4.9
|
||||||
custom_lint: ^0.6.4
|
custom_lint: ^0.6.4
|
||||||
|
|
@ -112,6 +113,7 @@ flutter:
|
||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
- assets/animations/
|
- assets/animations/
|
||||||
|
- assets/sounds/
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue