一堆乱七八糟的修改

播放页面增加桌面版
This commit is contained in:
rang 2025-11-28 17:05:35 +08:00
parent aee1fbde88
commit 3ba35b31b8
116 changed files with 1238 additions and 2592 deletions

View file

@ -0,0 +1,63 @@
// this provider is used to provide the Api settings to the app
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/features/settings/models/api_settings.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_settings_provider.g.dart';
final _box = AvailableHiveBoxes.apiSettingsBox;
final _logger = Logger('ApiSettingsProvider');
@Riverpod(keepAlive: true)
class ApiSettings extends _$ApiSettings {
@override
model.ApiSettings build() {
state = readFromBoxOrCreate();
ref.listenSelf((_, __) {
writeToBox();
});
return state;
}
model.ApiSettings readFromBoxOrCreate() {
// see if the settings are already in the box
if (_box.isNotEmpty) {
var foundSettings = _box.getAt(0);
// foundSettings.activeServer ??= foundSettings.activeUser?.server;
// foundSettings =foundSettings.copyWith(activeServer: foundSettings.activeUser?.server);
if (foundSettings.activeServer == null) {
foundSettings = foundSettings.copyWith(
activeServer: foundSettings.activeUser?.server,
);
}
_logger.fine('found api settings in box: ${foundSettings.obfuscate()}');
return foundSettings;
} else {
// create a new settings object
const settings = model.ApiSettings();
_logger.fine('created new api settings: $settings');
return settings;
}
}
// write the settings to the box
void writeToBox() {
_box.clear();
_box.add(state);
_logger.fine('wrote api settings to box: ${state.obfuscate()}');
}
void updateState(model.ApiSettings newSettings, {bool force = false}) {
// check if the settings are different
if (state == newSettings && !force) {
return;
}
state = newSettings;
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_settings_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$apiSettingsHash() => r'5bc1e16e9d72b77fb10637aabadf08e8947da580';
/// See also [ApiSettings].
@ProviderFor(ApiSettings)
final apiSettingsProvider =
NotifierProvider<ApiSettings, model.ApiSettings>.internal(
ApiSettings.new,
name: r'apiSettingsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ApiSettings = Notifier<model.ApiSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -0,0 +1,73 @@
// this provider is used to provide the app settings to the app
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/features/settings/models/app_settings.dart' as model;
part 'app_settings_provider.g.dart';
final _box = AvailableHiveBoxes.userPrefsBox;
final _logger = Logger('AppSettingsProvider');
model.AppSettings loadOrCreateAppSettings() {
// see if the settings are already in the box
model.AppSettings? settings;
if (_box.isNotEmpty) {
try {
settings = _box.getAt(0);
_logger.fine('found settings in box: $settings');
} catch (e) {
_logger.warning('error reading settings from box: $e'
'\nclearing box');
_box.clear();
}
} else {
_logger.fine('no settings found in box, creating new settings');
}
return settings ?? const model.AppSettings();
}
@Riverpod(keepAlive: true)
class AppSettings extends _$AppSettings {
@override
model.AppSettings build() {
state = loadOrCreateAppSettings();
ref.listenSelf((_, __) {
writeToBox();
});
return state;
}
// write the settings to the box
void writeToBox() {
_box.clear();
_box.add(state);
_logger.fine('wrote settings to box: $state');
}
void update(model.AppSettings newSettings) {
state = newSettings;
}
void reset() {
state = const model.AppSettings();
}
}
// SleepTimerSettings provider but only rebuilds when the sleep timer settings change
@Riverpod(keepAlive: true)
class SleepTimerSettings extends _$SleepTimerSettings {
@override
model.SleepTimerSettings build() {
final settings = ref.read(appSettingsProvider).sleepTimerSettings;
state = settings;
ref.listen(appSettingsProvider, (a, b) {
if (a?.sleepTimerSettings != b.sleepTimerSettings) {
state = b.sleepTimerSettings;
}
});
return state;
}
}

View file

@ -0,0 +1,42 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$appSettingsHash() => r'314d7936f54550f57d308056a99230402342a6d0';
/// See also [AppSettings].
@ProviderFor(AppSettings)
final appSettingsProvider =
NotifierProvider<AppSettings, model.AppSettings>.internal(
AppSettings.new,
name: r'appSettingsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppSettings = Notifier<model.AppSettings>;
String _$sleepTimerSettingsHash() =>
r'85bb3d3fb292b9a3a5b771d86e5fc57718519c69';
/// See also [SleepTimerSettings].
@ProviderFor(SleepTimerSettings)
final sleepTimerSettingsProvider =
NotifierProvider<SleepTimerSettings, model.SleepTimerSettings>.internal(
SleepTimerSettings.new,
name: r'sleepTimerSettingsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$sleepTimerSettingsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SleepTimerSettings = Notifier<model.SleepTimerSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -0,0 +1,23 @@
// a freezed class to store the settings of the app
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vaani/features/settings/models/audiobookshelf_server.dart';
import 'package:vaani/features/settings/models/authenticated_user.dart';
part 'api_settings.freezed.dart';
part 'api_settings.g.dart';
/// stores the settings for the active server and user
///
/// all settings that are needed to interact with the server are stored here
@freezed
class ApiSettings with _$ApiSettings {
const factory ApiSettings({
AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId,
}) = _ApiSettings;
factory ApiSettings.fromJson(Map<String, dynamic> json) =>
_$ApiSettingsFromJson(json);
}

View file

@ -0,0 +1,246 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'api_settings.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
ApiSettings _$ApiSettingsFromJson(Map<String, dynamic> json) {
return _ApiSettings.fromJson(json);
}
/// @nodoc
mixin _$ApiSettings {
AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError;
AuthenticatedUser? get activeUser => throw _privateConstructorUsedError;
String? get activeLibraryId => throw _privateConstructorUsedError;
/// Serializes this ApiSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ApiSettingsCopyWith<ApiSettings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ApiSettingsCopyWith<$Res> {
factory $ApiSettingsCopyWith(
ApiSettings value, $Res Function(ApiSettings) then) =
_$ApiSettingsCopyWithImpl<$Res, ApiSettings>;
@useResult
$Res call(
{AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId});
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
$AuthenticatedUserCopyWith<$Res>? get activeUser;
}
/// @nodoc
class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings>
implements $ApiSettingsCopyWith<$Res> {
_$ApiSettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeServer = freezed,
Object? activeUser = freezed,
Object? activeLibraryId = freezed,
}) {
return _then(_value.copyWith(
activeServer: freezed == activeServer
? _value.activeServer
: activeServer // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer?,
activeUser: freezed == activeUser
? _value.activeUser
: activeUser // ignore: cast_nullable_to_non_nullable
as AuthenticatedUser?,
activeLibraryId: freezed == activeLibraryId
? _value.activeLibraryId
: activeLibraryId // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res>? get activeServer {
if (_value.activeServer == null) {
return null;
}
return $AudiobookShelfServerCopyWith<$Res>(_value.activeServer!, (value) {
return _then(_value.copyWith(activeServer: value) as $Val);
});
}
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AuthenticatedUserCopyWith<$Res>? get activeUser {
if (_value.activeUser == null) {
return null;
}
return $AuthenticatedUserCopyWith<$Res>(_value.activeUser!, (value) {
return _then(_value.copyWith(activeUser: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ApiSettingsImplCopyWith<$Res>
implements $ApiSettingsCopyWith<$Res> {
factory _$$ApiSettingsImplCopyWith(
_$ApiSettingsImpl value, $Res Function(_$ApiSettingsImpl) then) =
__$$ApiSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AudiobookShelfServer? activeServer,
AuthenticatedUser? activeUser,
String? activeLibraryId});
@override
$AudiobookShelfServerCopyWith<$Res>? get activeServer;
@override
$AuthenticatedUserCopyWith<$Res>? get activeUser;
}
/// @nodoc
class __$$ApiSettingsImplCopyWithImpl<$Res>
extends _$ApiSettingsCopyWithImpl<$Res, _$ApiSettingsImpl>
implements _$$ApiSettingsImplCopyWith<$Res> {
__$$ApiSettingsImplCopyWithImpl(
_$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then)
: super(_value, _then);
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeServer = freezed,
Object? activeUser = freezed,
Object? activeLibraryId = freezed,
}) {
return _then(_$ApiSettingsImpl(
activeServer: freezed == activeServer
? _value.activeServer
: activeServer // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer?,
activeUser: freezed == activeUser
? _value.activeUser
: activeUser // ignore: cast_nullable_to_non_nullable
as AuthenticatedUser?,
activeLibraryId: freezed == activeLibraryId
? _value.activeLibraryId
: activeLibraryId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ApiSettingsImpl implements _ApiSettings {
const _$ApiSettingsImpl(
{this.activeServer, this.activeUser, this.activeLibraryId});
factory _$ApiSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$ApiSettingsImplFromJson(json);
@override
final AudiobookShelfServer? activeServer;
@override
final AuthenticatedUser? activeUser;
@override
final String? activeLibraryId;
@override
String toString() {
return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ApiSettingsImpl &&
(identical(other.activeServer, activeServer) ||
other.activeServer == activeServer) &&
(identical(other.activeUser, activeUser) ||
other.activeUser == activeUser) &&
(identical(other.activeLibraryId, activeLibraryId) ||
other.activeLibraryId == activeLibraryId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, activeServer, activeUser, activeLibraryId);
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
__$$ApiSettingsImplCopyWithImpl<_$ApiSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ApiSettingsImplToJson(
this,
);
}
}
abstract class _ApiSettings implements ApiSettings {
const factory _ApiSettings(
{final AudiobookShelfServer? activeServer,
final AuthenticatedUser? activeUser,
final String? activeLibraryId}) = _$ApiSettingsImpl;
factory _ApiSettings.fromJson(Map<String, dynamic> json) =
_$ApiSettingsImpl.fromJson;
@override
AudiobookShelfServer? get activeServer;
@override
AuthenticatedUser? get activeUser;
@override
String? get activeLibraryId;
/// Create a copy of ApiSettings
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map<String, dynamic> json) =>
_$ApiSettingsImpl(
activeServer: json['activeServer'] == null
? null
: AudiobookShelfServer.fromJson(
json['activeServer'] as Map<String, dynamic>),
activeUser: json['activeUser'] == null
? null
: AuthenticatedUser.fromJson(
json['activeUser'] as Map<String, dynamic>),
activeLibraryId: json['activeLibraryId'] as String?,
);
Map<String, dynamic> _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
<String, dynamic>{
'activeServer': instance.activeServer,
'activeUser': instance.activeUser,
'activeLibraryId': instance.activeLibraryId,
};

View file

@ -0,0 +1,247 @@
// a freezed class to store the settings of the app
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_settings.freezed.dart';
part 'app_settings.g.dart';
/// stores the settings of the app
///
/// only the visual settings are stored here
@freezed
class AppSettings with _$AppSettings {
const factory AppSettings({
@Default('zh') String language,
@Default(ThemeSettings()) ThemeSettings themeSettings,
@Default(PlayerSettings()) PlayerSettings playerSettings,
@Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings,
@Default(DownloadSettings()) DownloadSettings downloadSettings,
@Default(NotificationSettings()) NotificationSettings notificationSettings,
@Default(ShakeDetectionSettings())
ShakeDetectionSettings shakeDetectionSettings,
@Default(HomePageSettings()) HomePageSettings homePageSettings,
}) = _AppSettings;
factory AppSettings.fromJson(Map<String, dynamic> json) =>
_$AppSettingsFromJson(json);
}
@freezed
class ThemeSettings with _$ThemeSettings {
const factory ThemeSettings({
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(false) bool highContrast,
@Default(false) bool useMaterialThemeFromSystem,
@Default('#FF311B92') String customThemeColor,
@Default(true) bool useMaterialThemeOnItemPage,
@Default(true) bool useCurrentPlayerThemeThroughoutApp,
}) = _ThemeSettings;
factory ThemeSettings.fromJson(Map<String, dynamic> json) =>
_$ThemeSettingsFromJson(json);
}
@freezed
class PlayerSettings with _$PlayerSettings {
const factory PlayerSettings({
@Default(MinimizedPlayerSettings())
MinimizedPlayerSettings miniPlayerSettings,
@Default(ExpandedPlayerSettings())
ExpandedPlayerSettings expandedPlayerSettings,
@Default(1) double preferredDefaultVolume,
@Default(1) double preferredDefaultSpeed,
@Default([1, 1.25, 1.5, 1.75, 2]) List<double> speedOptions,
@Default(0.05) double speedIncrement,
@Default(0.1) double minSpeed,
@Default(4) double maxSpeed,
@Default(Duration(seconds: 10)) Duration minimumPositionForReporting,
@Default(Duration(seconds: 10)) Duration playbackReportInterval,
@Default(Duration(seconds: 15)) Duration markCompleteWhenTimeLeft,
@Default(true) bool configurePlayerForEveryBook,
}) = _PlayerSettings;
factory PlayerSettings.fromJson(Map<String, dynamic> json) =>
_$PlayerSettingsFromJson(json);
}
@freezed
class ExpandedPlayerSettings with _$ExpandedPlayerSettings {
const factory ExpandedPlayerSettings({
@Default(false) bool showTotalProgress,
@Default(true) bool showChapterProgress,
}) = _ExpandedPlayerSettings;
factory ExpandedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$ExpandedPlayerSettingsFromJson(json);
}
@freezed
class MinimizedPlayerSettings with _$MinimizedPlayerSettings {
const factory MinimizedPlayerSettings({
@Default(false) bool useChapterInfo,
}) = _MinimizedPlayerSettings;
factory MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$MinimizedPlayerSettingsFromJson(json);
}
@freezed
class SleepTimerSettings with _$SleepTimerSettings {
const factory SleepTimerSettings({
@Default(Duration(minutes: 15)) Duration defaultDuration,
@Default(
[
Duration(minutes: 5),
Duration(minutes: 10),
Duration(minutes: 15),
Duration(minutes: 20),
Duration(minutes: 30),
],
)
List<Duration> presetDurations,
@Default(Duration(minutes: 100)) Duration maxDuration,
@Default(false) bool fadeOutAudio,
@Default(Duration(seconds: 20)) Duration fadeOutDuration,
/// if true, the player will automatically rewind the audio when the sleep timer is stopped
@Default(false) bool autoRewindWhenStopped,
/// the key is the duration in minutes
@Default({
5: Duration(seconds: 10),
15: Duration(seconds: 30),
45: Duration(seconds: 45),
60: Duration(minutes: 1),
120: Duration(minutes: 2),
})
Map<int, Duration> autoRewindDurations,
/// auto turn on timer settings
@Default(false) bool autoTurnOnTimer,
/// always auto turn on timer settings or during specific times
@Default(false) bool alwaysAutoTurnOnTimer,
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
///
/// duration is the time from 00:00
@Default(Duration(hours: 22, minutes: 0)) Duration autoTurnOnTime,
@Default(Duration(hours: 6, minutes: 0)) Duration autoTurnOffTime,
}) = _SleepTimerSettings;
factory SleepTimerSettings.fromJson(Map<String, dynamic> json) =>
_$SleepTimerSettingsFromJson(json);
}
@freezed
class DownloadSettings with _$DownloadSettings {
const factory DownloadSettings({
@Default(true) bool requiresWiFi,
@Default(3) int retries,
@Default(true) bool allowPause,
@Default(3) int maxConcurrent,
@Default(3) int maxConcurrentByHost,
@Default(3) int maxConcurrentByGroup,
}) = _DownloadSettings;
factory DownloadSettings.fromJson(Map<String, dynamic> json) =>
_$DownloadSettingsFromJson(json);
}
@freezed
class NotificationSettings with _$NotificationSettings {
const factory NotificationSettings({
@Default(Duration(seconds: 30)) Duration fastForwardInterval,
@Default(Duration(seconds: 10)) Duration rewindInterval,
@Default(true) bool progressBarIsChapterProgress,
@Default('\$bookTitle') String primaryTitle,
@Default('\$author') String secondaryTitle,
@Default(
[
NotificationMediaControl.rewind,
NotificationMediaControl.fastForward,
NotificationMediaControl.skipToPreviousChapter,
NotificationMediaControl.skipToNextChapter,
],
)
List<NotificationMediaControl> mediaControls,
}) = _NotificationSettings;
factory NotificationSettings.fromJson(Map<String, dynamic> json) =>
_$NotificationSettingsFromJson(json);
}
enum NotificationTitleType {
chapterTitle,
bookTitle,
author,
subtitle,
series,
narrator,
year,
}
enum NotificationMediaControl {
fastForward(Icons.fast_forward),
rewind(Icons.fast_rewind),
speedToggle(Icons.speed),
stop(Icons.stop),
skipToNextChapter(Icons.skip_next),
skipToPreviousChapter(Icons.skip_previous);
const NotificationMediaControl(this.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})
Set<ShakeDetectedFeedback> feedback,
@Default(0.5) double beepVolume,
/// the duration to wait before the shake detection is enabled again
@Default(Duration(seconds: 2)) 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 }
@freezed
class HomePageSettings with _$HomePageSettings {
const factory HomePageSettings({
@Default(true) bool showPlayButtonOnContinueListeningShelf,
@Default(false) bool showPlayButtonOnContinueSeriesShelf,
@Default(false) bool showPlayButtonOnAllRemainingShelves,
@Default(false) bool showPlayButtonOnListenAgainShelf,
}) = _HomePageSettings;
factory HomePageSettings.fromJson(Map<String, dynamic> json) =>
_$HomePageSettingsFromJson(json);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,386 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AppSettingsImpl _$$AppSettingsImplFromJson(Map<String, dynamic> json) =>
_$AppSettingsImpl(
language: json['language'] as String? ?? 'zh',
themeSettings: json['themeSettings'] == null
? const ThemeSettings()
: ThemeSettings.fromJson(
json['themeSettings'] as Map<String, dynamic>),
playerSettings: json['playerSettings'] == null
? const PlayerSettings()
: PlayerSettings.fromJson(
json['playerSettings'] as Map<String, dynamic>),
sleepTimerSettings: json['sleepTimerSettings'] == null
? const SleepTimerSettings()
: SleepTimerSettings.fromJson(
json['sleepTimerSettings'] as Map<String, dynamic>),
downloadSettings: json['downloadSettings'] == null
? const DownloadSettings()
: DownloadSettings.fromJson(
json['downloadSettings'] as Map<String, dynamic>),
notificationSettings: json['notificationSettings'] == null
? const NotificationSettings()
: NotificationSettings.fromJson(
json['notificationSettings'] as Map<String, dynamic>),
shakeDetectionSettings: json['shakeDetectionSettings'] == null
? const ShakeDetectionSettings()
: ShakeDetectionSettings.fromJson(
json['shakeDetectionSettings'] as Map<String, dynamic>),
homePageSettings: json['homePageSettings'] == null
? const HomePageSettings()
: HomePageSettings.fromJson(
json['homePageSettings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$AppSettingsImplToJson(_$AppSettingsImpl instance) =>
<String, dynamic>{
'language': instance.language,
'themeSettings': instance.themeSettings,
'playerSettings': instance.playerSettings,
'sleepTimerSettings': instance.sleepTimerSettings,
'downloadSettings': instance.downloadSettings,
'notificationSettings': instance.notificationSettings,
'shakeDetectionSettings': instance.shakeDetectionSettings,
'homePageSettings': instance.homePageSettings,
};
_$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map<String, dynamic> json) =>
_$ThemeSettingsImpl(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system,
highContrast: json['highContrast'] as bool? ?? false,
useMaterialThemeFromSystem:
json['useMaterialThemeFromSystem'] as bool? ?? false,
customThemeColor: json['customThemeColor'] as String? ?? '#FF311B92',
useMaterialThemeOnItemPage:
json['useMaterialThemeOnItemPage'] as bool? ?? true,
useCurrentPlayerThemeThroughoutApp:
json['useCurrentPlayerThemeThroughoutApp'] as bool? ?? true,
);
Map<String, dynamic> _$$ThemeSettingsImplToJson(_$ThemeSettingsImpl instance) =>
<String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'highContrast': instance.highContrast,
'useMaterialThemeFromSystem': instance.useMaterialThemeFromSystem,
'customThemeColor': instance.customThemeColor,
'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage,
'useCurrentPlayerThemeThroughoutApp':
instance.useCurrentPlayerThemeThroughoutApp,
};
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};
_$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
_$PlayerSettingsImpl(
miniPlayerSettings: json['miniPlayerSettings'] == null
? const MinimizedPlayerSettings()
: MinimizedPlayerSettings.fromJson(
json['miniPlayerSettings'] as Map<String, dynamic>),
expandedPlayerSettings: json['expandedPlayerSettings'] == null
? const ExpandedPlayerSettings()
: ExpandedPlayerSettings.fromJson(
json['expandedPlayerSettings'] as Map<String, dynamic>),
preferredDefaultVolume:
(json['preferredDefaultVolume'] as num?)?.toDouble() ?? 1,
preferredDefaultSpeed:
(json['preferredDefaultSpeed'] as num?)?.toDouble() ?? 1,
speedOptions: (json['speedOptions'] as List<dynamic>?)
?.map((e) => (e as num).toDouble())
.toList() ??
const [1, 1.25, 1.5, 1.75, 2],
speedIncrement: (json['speedIncrement'] as num?)?.toDouble() ?? 0.05,
minSpeed: (json['minSpeed'] as num?)?.toDouble() ?? 0.1,
maxSpeed: (json['maxSpeed'] as num?)?.toDouble() ?? 4,
minimumPositionForReporting: json['minimumPositionForReporting'] == null
? const Duration(seconds: 10)
: Duration(
microseconds:
(json['minimumPositionForReporting'] as num).toInt()),
playbackReportInterval: json['playbackReportInterval'] == null
? const Duration(seconds: 10)
: Duration(
microseconds: (json['playbackReportInterval'] as num).toInt()),
markCompleteWhenTimeLeft: json['markCompleteWhenTimeLeft'] == null
? const Duration(seconds: 15)
: Duration(
microseconds: (json['markCompleteWhenTimeLeft'] as num).toInt()),
configurePlayerForEveryBook:
json['configurePlayerForEveryBook'] as bool? ?? true,
);
Map<String, dynamic> _$$PlayerSettingsImplToJson(
_$PlayerSettingsImpl instance) =>
<String, dynamic>{
'miniPlayerSettings': instance.miniPlayerSettings,
'expandedPlayerSettings': instance.expandedPlayerSettings,
'preferredDefaultVolume': instance.preferredDefaultVolume,
'preferredDefaultSpeed': instance.preferredDefaultSpeed,
'speedOptions': instance.speedOptions,
'speedIncrement': instance.speedIncrement,
'minSpeed': instance.minSpeed,
'maxSpeed': instance.maxSpeed,
'minimumPositionForReporting':
instance.minimumPositionForReporting.inMicroseconds,
'playbackReportInterval': instance.playbackReportInterval.inMicroseconds,
'markCompleteWhenTimeLeft':
instance.markCompleteWhenTimeLeft.inMicroseconds,
'configurePlayerForEveryBook': instance.configurePlayerForEveryBook,
};
_$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$ExpandedPlayerSettingsImpl(
showTotalProgress: json['showTotalProgress'] as bool? ?? false,
showChapterProgress: json['showChapterProgress'] as bool? ?? true,
);
Map<String, dynamic> _$$ExpandedPlayerSettingsImplToJson(
_$ExpandedPlayerSettingsImpl instance) =>
<String, dynamic>{
'showTotalProgress': instance.showTotalProgress,
'showChapterProgress': instance.showChapterProgress,
};
_$MinimizedPlayerSettingsImpl _$$MinimizedPlayerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$MinimizedPlayerSettingsImpl(
useChapterInfo: json['useChapterInfo'] as bool? ?? false,
);
Map<String, dynamic> _$$MinimizedPlayerSettingsImplToJson(
_$MinimizedPlayerSettingsImpl instance) =>
<String, dynamic>{
'useChapterInfo': instance.useChapterInfo,
};
_$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$SleepTimerSettingsImpl(
defaultDuration: json['defaultDuration'] == null
? const Duration(minutes: 15)
: Duration(microseconds: (json['defaultDuration'] as num).toInt()),
presetDurations: (json['presetDurations'] as List<dynamic>?)
?.map((e) => Duration(microseconds: (e as num).toInt()))
.toList() ??
const [
Duration(minutes: 5),
Duration(minutes: 10),
Duration(minutes: 15),
Duration(minutes: 20),
Duration(minutes: 30)
],
maxDuration: json['maxDuration'] == null
? const Duration(minutes: 100)
: Duration(microseconds: (json['maxDuration'] as num).toInt()),
fadeOutAudio: json['fadeOutAudio'] as bool? ?? false,
fadeOutDuration: json['fadeOutDuration'] == null
? const Duration(seconds: 20)
: Duration(microseconds: (json['fadeOutDuration'] as num).toInt()),
autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false,
autoRewindDurations:
(json['autoRewindDurations'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(
int.parse(k), Duration(microseconds: (e as num).toInt())),
) ??
const {
5: Duration(seconds: 10),
15: Duration(seconds: 30),
45: Duration(seconds: 45),
60: Duration(minutes: 1),
120: Duration(minutes: 2)
},
autoTurnOnTimer: json['autoTurnOnTimer'] as bool? ?? false,
alwaysAutoTurnOnTimer: json['alwaysAutoTurnOnTimer'] as bool? ?? false,
autoTurnOnTime: json['autoTurnOnTime'] == null
? const Duration(hours: 22, minutes: 0)
: Duration(microseconds: (json['autoTurnOnTime'] as num).toInt()),
autoTurnOffTime: json['autoTurnOffTime'] == null
? const Duration(hours: 6, minutes: 0)
: Duration(microseconds: (json['autoTurnOffTime'] as num).toInt()),
);
Map<String, dynamic> _$$SleepTimerSettingsImplToJson(
_$SleepTimerSettingsImpl instance) =>
<String, dynamic>{
'defaultDuration': instance.defaultDuration.inMicroseconds,
'presetDurations':
instance.presetDurations.map((e) => e.inMicroseconds).toList(),
'maxDuration': instance.maxDuration.inMicroseconds,
'fadeOutAudio': instance.fadeOutAudio,
'fadeOutDuration': instance.fadeOutDuration.inMicroseconds,
'autoRewindWhenStopped': instance.autoRewindWhenStopped,
'autoRewindDurations': instance.autoRewindDurations
.map((k, e) => MapEntry(k.toString(), e.inMicroseconds)),
'autoTurnOnTimer': instance.autoTurnOnTimer,
'alwaysAutoTurnOnTimer': instance.alwaysAutoTurnOnTimer,
'autoTurnOnTime': instance.autoTurnOnTime.inMicroseconds,
'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds,
};
_$DownloadSettingsImpl _$$DownloadSettingsImplFromJson(
Map<String, dynamic> json) =>
_$DownloadSettingsImpl(
requiresWiFi: json['requiresWiFi'] as bool? ?? true,
retries: (json['retries'] as num?)?.toInt() ?? 3,
allowPause: json['allowPause'] as bool? ?? true,
maxConcurrent: (json['maxConcurrent'] as num?)?.toInt() ?? 3,
maxConcurrentByHost: (json['maxConcurrentByHost'] as num?)?.toInt() ?? 3,
maxConcurrentByGroup:
(json['maxConcurrentByGroup'] as num?)?.toInt() ?? 3,
);
Map<String, dynamic> _$$DownloadSettingsImplToJson(
_$DownloadSettingsImpl instance) =>
<String, dynamic>{
'requiresWiFi': instance.requiresWiFi,
'retries': instance.retries,
'allowPause': instance.allowPause,
'maxConcurrent': instance.maxConcurrent,
'maxConcurrentByHost': instance.maxConcurrentByHost,
'maxConcurrentByGroup': instance.maxConcurrentByGroup,
};
_$NotificationSettingsImpl _$$NotificationSettingsImplFromJson(
Map<String, dynamic> json) =>
_$NotificationSettingsImpl(
fastForwardInterval: json['fastForwardInterval'] == null
? const Duration(seconds: 30)
: Duration(
microseconds: (json['fastForwardInterval'] as num).toInt()),
rewindInterval: json['rewindInterval'] == null
? const Duration(seconds: 10)
: Duration(microseconds: (json['rewindInterval'] as num).toInt()),
progressBarIsChapterProgress:
json['progressBarIsChapterProgress'] as bool? ?? true,
primaryTitle: json['primaryTitle'] as String? ?? '\$bookTitle',
secondaryTitle: json['secondaryTitle'] as String? ?? '\$author',
mediaControls: (json['mediaControls'] as List<dynamic>?)
?.map((e) => $enumDecode(_$NotificationMediaControlEnumMap, e))
.toList() ??
const [
NotificationMediaControl.rewind,
NotificationMediaControl.fastForward,
NotificationMediaControl.skipToPreviousChapter,
NotificationMediaControl.skipToNextChapter
],
);
Map<String, dynamic> _$$NotificationSettingsImplToJson(
_$NotificationSettingsImpl instance) =>
<String, dynamic>{
'fastForwardInterval': instance.fastForwardInterval.inMicroseconds,
'rewindInterval': instance.rewindInterval.inMicroseconds,
'progressBarIsChapterProgress': instance.progressBarIsChapterProgress,
'primaryTitle': instance.primaryTitle,
'secondaryTitle': instance.secondaryTitle,
'mediaControls': instance.mediaControls
.map((e) => _$NotificationMediaControlEnumMap[e]!)
.toList(),
};
const _$NotificationMediaControlEnumMap = {
NotificationMediaControl.fastForward: 'fastForward',
NotificationMediaControl.rewind: 'rewind',
NotificationMediaControl.speedToggle: 'speedToggle',
NotificationMediaControl.stop: 'stop',
NotificationMediaControl.skipToNextChapter: 'skipToNextChapter',
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},
beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5,
shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null
? const Duration(seconds: 2)
: 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',
};
_$HomePageSettingsImpl _$$HomePageSettingsImplFromJson(
Map<String, dynamic> json) =>
_$HomePageSettingsImpl(
showPlayButtonOnContinueListeningShelf:
json['showPlayButtonOnContinueListeningShelf'] as bool? ?? true,
showPlayButtonOnContinueSeriesShelf:
json['showPlayButtonOnContinueSeriesShelf'] as bool? ?? false,
showPlayButtonOnAllRemainingShelves:
json['showPlayButtonOnAllRemainingShelves'] as bool? ?? false,
showPlayButtonOnListenAgainShelf:
json['showPlayButtonOnListenAgainShelf'] as bool? ?? false,
);
Map<String, dynamic> _$$HomePageSettingsImplToJson(
_$HomePageSettingsImpl instance) =>
<String, dynamic>{
'showPlayButtonOnContinueListeningShelf':
instance.showPlayButtonOnContinueListeningShelf,
'showPlayButtonOnContinueSeriesShelf':
instance.showPlayButtonOnContinueSeriesShelf,
'showPlayButtonOnAllRemainingShelves':
instance.showPlayButtonOnAllRemainingShelves,
'showPlayButtonOnListenAgainShelf':
instance.showPlayButtonOnListenAgainShelf,
};

View file

@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'audiobookshelf_server.freezed.dart';
part 'audiobookshelf_server.g.dart';
typedef AudiobookShelfUri = Uri;
/// Represents a audiobookshelf server
@freezed
class AudiobookShelfServer with _$AudiobookShelfServer {
const factory AudiobookShelfServer({
required AudiobookShelfUri serverUrl,
// String? serverName,
}) = _AudiobookShelfServer;
factory AudiobookShelfServer.fromJson(Map<String, dynamic> json) =>
_$AudiobookShelfServerFromJson(json);
}

View file

@ -0,0 +1,169 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'audiobookshelf_server.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AudiobookShelfServer _$AudiobookShelfServerFromJson(Map<String, dynamic> json) {
return _AudiobookShelfServer.fromJson(json);
}
/// @nodoc
mixin _$AudiobookShelfServer {
Uri get serverUrl => throw _privateConstructorUsedError;
/// Serializes this AudiobookShelfServer to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AudiobookShelfServerCopyWith<AudiobookShelfServer> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AudiobookShelfServerCopyWith<$Res> {
factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value,
$Res Function(AudiobookShelfServer) then) =
_$AudiobookShelfServerCopyWithImpl<$Res, AudiobookShelfServer>;
@useResult
$Res call({Uri serverUrl});
}
/// @nodoc
class _$AudiobookShelfServerCopyWithImpl<$Res,
$Val extends AudiobookShelfServer>
implements $AudiobookShelfServerCopyWith<$Res> {
_$AudiobookShelfServerCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? serverUrl = null,
}) {
return _then(_value.copyWith(
serverUrl: null == serverUrl
? _value.serverUrl
: serverUrl // ignore: cast_nullable_to_non_nullable
as Uri,
) as $Val);
}
}
/// @nodoc
abstract class _$$AudiobookShelfServerImplCopyWith<$Res>
implements $AudiobookShelfServerCopyWith<$Res> {
factory _$$AudiobookShelfServerImplCopyWith(_$AudiobookShelfServerImpl value,
$Res Function(_$AudiobookShelfServerImpl) then) =
__$$AudiobookShelfServerImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({Uri serverUrl});
}
/// @nodoc
class __$$AudiobookShelfServerImplCopyWithImpl<$Res>
extends _$AudiobookShelfServerCopyWithImpl<$Res, _$AudiobookShelfServerImpl>
implements _$$AudiobookShelfServerImplCopyWith<$Res> {
__$$AudiobookShelfServerImplCopyWithImpl(_$AudiobookShelfServerImpl _value,
$Res Function(_$AudiobookShelfServerImpl) _then)
: super(_value, _then);
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? serverUrl = null,
}) {
return _then(_$AudiobookShelfServerImpl(
serverUrl: null == serverUrl
? _value.serverUrl
: serverUrl // ignore: cast_nullable_to_non_nullable
as Uri,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AudiobookShelfServerImpl implements _AudiobookShelfServer {
const _$AudiobookShelfServerImpl({required this.serverUrl});
factory _$AudiobookShelfServerImpl.fromJson(Map<String, dynamic> json) =>
_$$AudiobookShelfServerImplFromJson(json);
@override
final Uri serverUrl;
@override
String toString() {
return 'AudiobookShelfServer(serverUrl: $serverUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AudiobookShelfServerImpl &&
(identical(other.serverUrl, serverUrl) ||
other.serverUrl == serverUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, serverUrl);
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
get copyWith =>
__$$AudiobookShelfServerImplCopyWithImpl<_$AudiobookShelfServerImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AudiobookShelfServerImplToJson(
this,
);
}
}
abstract class _AudiobookShelfServer implements AudiobookShelfServer {
const factory _AudiobookShelfServer({required final Uri serverUrl}) =
_$AudiobookShelfServerImpl;
factory _AudiobookShelfServer.fromJson(Map<String, dynamic> json) =
_$AudiobookShelfServerImpl.fromJson;
@override
Uri get serverUrl;
/// Create a copy of AudiobookShelfServer
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audiobookshelf_server.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
Map<String, dynamic> json) =>
_$AudiobookShelfServerImpl(
serverUrl: Uri.parse(json['serverUrl'] as String),
);
Map<String, dynamic> _$$AudiobookShelfServerImplToJson(
_$AudiobookShelfServerImpl instance) =>
<String, dynamic>{
'serverUrl': instance.serverUrl.toString(),
};

View file

@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vaani/features/settings/models/audiobookshelf_server.dart';
part 'authenticated_user.freezed.dart';
part 'authenticated_user.g.dart';
/// authenticated user with server and credentials
@freezed
class AuthenticatedUser with _$AuthenticatedUser {
const factory AuthenticatedUser({
required AudiobookShelfServer server,
required String authToken,
required String id,
String? username,
}) = _AuthenticatedUser;
factory AuthenticatedUser.fromJson(Map<String, dynamic> json) =>
_$AuthenticatedUserFromJson(json);
}

View file

@ -0,0 +1,246 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'authenticated_user.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AuthenticatedUser _$AuthenticatedUserFromJson(Map<String, dynamic> json) {
return _AuthenticatedUser.fromJson(json);
}
/// @nodoc
mixin _$AuthenticatedUser {
AudiobookShelfServer get server => throw _privateConstructorUsedError;
String get authToken => throw _privateConstructorUsedError;
String get id => throw _privateConstructorUsedError;
String? get username => throw _privateConstructorUsedError;
/// Serializes this AuthenticatedUser to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AuthenticatedUserCopyWith<AuthenticatedUser> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AuthenticatedUserCopyWith<$Res> {
factory $AuthenticatedUserCopyWith(
AuthenticatedUser value, $Res Function(AuthenticatedUser) then) =
_$AuthenticatedUserCopyWithImpl<$Res, AuthenticatedUser>;
@useResult
$Res call(
{AudiobookShelfServer server,
String authToken,
String id,
String? username});
$AudiobookShelfServerCopyWith<$Res> get server;
}
/// @nodoc
class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
implements $AuthenticatedUserCopyWith<$Res> {
_$AuthenticatedUserCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? server = null,
Object? authToken = null,
Object? id = null,
Object? username = freezed,
}) {
return _then(_value.copyWith(
server: null == server
? _value.server
: server // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer,
authToken: null == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
username: freezed == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AudiobookShelfServerCopyWith<$Res> get server {
return $AudiobookShelfServerCopyWith<$Res>(_value.server, (value) {
return _then(_value.copyWith(server: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AuthenticatedUserImplCopyWith<$Res>
implements $AuthenticatedUserCopyWith<$Res> {
factory _$$AuthenticatedUserImplCopyWith(_$AuthenticatedUserImpl value,
$Res Function(_$AuthenticatedUserImpl) then) =
__$$AuthenticatedUserImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AudiobookShelfServer server,
String authToken,
String id,
String? username});
@override
$AudiobookShelfServerCopyWith<$Res> get server;
}
/// @nodoc
class __$$AuthenticatedUserImplCopyWithImpl<$Res>
extends _$AuthenticatedUserCopyWithImpl<$Res, _$AuthenticatedUserImpl>
implements _$$AuthenticatedUserImplCopyWith<$Res> {
__$$AuthenticatedUserImplCopyWithImpl(_$AuthenticatedUserImpl _value,
$Res Function(_$AuthenticatedUserImpl) _then)
: super(_value, _then);
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? server = null,
Object? authToken = null,
Object? id = null,
Object? username = freezed,
}) {
return _then(_$AuthenticatedUserImpl(
server: null == server
? _value.server
: server // ignore: cast_nullable_to_non_nullable
as AudiobookShelfServer,
authToken: null == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
username: freezed == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AuthenticatedUserImpl implements _AuthenticatedUser {
const _$AuthenticatedUserImpl(
{required this.server,
required this.authToken,
required this.id,
this.username});
factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthenticatedUserImplFromJson(json);
@override
final AudiobookShelfServer server;
@override
final String authToken;
@override
final String id;
@override
final String? username;
@override
String toString() {
return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthenticatedUserImpl &&
(identical(other.server, server) || other.server == server) &&
(identical(other.authToken, authToken) ||
other.authToken == authToken) &&
(identical(other.id, id) || other.id == id) &&
(identical(other.username, username) ||
other.username == username));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, server, authToken, id, username);
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
__$$AuthenticatedUserImplCopyWithImpl<_$AuthenticatedUserImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AuthenticatedUserImplToJson(
this,
);
}
}
abstract class _AuthenticatedUser implements AuthenticatedUser {
const factory _AuthenticatedUser(
{required final AudiobookShelfServer server,
required final String authToken,
required final String id,
final String? username}) = _$AuthenticatedUserImpl;
factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) =
_$AuthenticatedUserImpl.fromJson;
@override
AudiobookShelfServer get server;
@override
String get authToken;
@override
String get id;
@override
String? get username;
/// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticated_user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
Map<String, dynamic> json) =>
_$AuthenticatedUserImpl(
server:
AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>),
authToken: json['authToken'] as String,
id: json['id'] as String,
username: json['username'] as String?,
);
Map<String, dynamic> _$$AuthenticatedUserImplToJson(
_$AuthenticatedUserImpl instance) =>
<String, dynamic>{
'server': instance.server,
'authToken': instance.authToken,
'id': instance.id,
'username': instance.username,
};

View file

@ -0,0 +1,4 @@
export 'api_settings.dart';
export 'app_settings.dart';
export 'audiobookshelf_server.dart';
export 'authenticated_user.dart';

View file

@ -0,0 +1 @@
export 'app_settings_provider.dart';

View file

@ -0,0 +1,316 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/models/app_settings.dart' as model;
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/features/settings/view/widgets/navigation_with_switch_tile.dart';
class AppSettingsPage extends HookConsumerWidget {
const AppSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final sleepTimerSettings = appSettings.sleepTimerSettings;
final locales = {'en': 'English', 'zh': '中文'};
return SimpleSettingsPage(
title: Text(S.of(context).appSettings),
sections: [
// General section
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
title: Text(
S.of(context).general,
style: Theme.of(context).textTheme.titleLarge,
),
tiles: [
SettingsTile(
title: Text(S.of(context).language),
leading: const Icon(Icons.play_arrow),
trailing: DropdownButton(
value: appSettings.language,
items: S.delegate.supportedLocales.map((locale) {
return DropdownMenuItem(
value: locale.languageCode,
child: Text(locales[locale.languageCode] ?? 'unknown'),
);
}).toList(),
onChanged: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith(
language: value!,
),
);
},
),
description: Text(S.of(context).languageDescription),
),
SettingsTile(
title: Text(S.of(context).playerSettings),
leading: const Icon(Icons.play_arrow),
description: Text(S.of(context).playerSettingsDescription),
onPressed: (context) {
context.pushNamed(Routes.playerSettings.name);
},
),
NavigationWithSwitchTile(
title: Text(S.of(context).autoTurnOnSleepTimer),
description: Text(S.of(context).automaticallyDescription),
leading: sleepTimerSettings.autoTurnOnTimer
? const Icon(Icons.timer, fill: 1)
: const Icon(Icons.timer_off, fill: 1),
onPressed: (context) {
context.pushNamed(Routes.autoSleepTimerSettings.name);
},
value: sleepTimerSettings.autoTurnOnTimer,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings(
autoTurnOnTimer: value,
),
);
},
),
NavigationWithSwitchTile(
title: Text(S.of(context).shakeDetector),
leading: const Icon(Icons.vibration),
description: Text(
S.of(context).shakeDetectorDescription,
),
value: appSettings.shakeDetectionSettings.isEnabled,
onPressed: (context) {
context.pushNamed(Routes.shakeDetectorSettings.name);
},
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.shakeDetectionSettings(
isEnabled: value,
),
);
},
),
],
),
// Appearance section
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
title: Text(
S.of(context).appearance,
style: Theme.of(context).textTheme.titleLarge,
),
tiles: [
SettingsTile.navigation(
leading: const Icon(Icons.color_lens),
title: Text(S.of(context).themeSettings),
description: Text(S.of(context).themeSettingsDescription),
onPressed: (context) {
context.pushNamed(Routes.themeSettings.name);
},
),
SettingsTile(
title: Text(S.of(context).notificationMediaPlayer),
leading: const Icon(Icons.play_lesson),
description:
Text(S.of(context).notificationMediaPlayerDescription),
onPressed: (context) {
context.pushNamed(Routes.notificationSettings.name);
},
),
SettingsTile.navigation(
leading: const Icon(Icons.home_filled),
title: Text(S.of(context).homePageSettings),
description: Text(S.of(context).homePageSettingsDescription),
onPressed: (context) {
context.pushNamed(Routes.homePageSettings.name);
},
),
],
),
// Backup and Restore section
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
title: Text(
S.of(context).backupAndRestore,
style: Theme.of(context).textTheme.titleLarge,
),
tiles: [
SettingsTile(
title: Text(S.of(context).copyToClipboard),
leading: const Icon(Icons.copy),
description: Text(
S.of(context).copyToClipboardDescription,
),
onPressed: (context) async {
// copy to clipboard
await Clipboard.setData(
ClipboardData(
text: jsonEncode(appSettings.toJson()),
),
);
// show toast
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context).copyToClipboardToast),
),
);
},
),
SettingsTile(
title: Text(S.of(context).restore),
leading: const Icon(Icons.restore),
description: Text(S.of(context).restoreDescription),
onPressed: (context) {
// show a dialog to get the backup
showDialog(
context: context,
builder: (context) {
return RestoreDialogue();
},
);
},
),
// a button to reset the app settings
SettingsTile(
title: Text(S.of(context).resetAppSettings),
leading: const Icon(Icons.settings_backup_restore),
description: Text(S.of(context).resetAppSettingsDescription),
onPressed: (context) async {
// confirm the reset
final res = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(S.of(context).resetAppSettings),
content: Text(S.of(context).resetAppSettingsDialog),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(S.of(context).reset),
),
],
);
},
);
// if the user confirms the reset
if (res == true) {
ref.read(appSettingsProvider.notifier).reset();
}
},
),
],
),
],
);
}
}
class RestoreDialogue extends HookConsumerWidget {
const RestoreDialogue({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final settings = useState<model.AppSettings?>(null);
final settingsInputController = useTextEditingController();
return AlertDialog(
title: Text(S.of(context).restoreBackup),
content: Form(
key: formKey,
child: TextFormField(
autofocus: true,
decoration: InputDecoration(
labelText: S.of(context).backup,
hintText: S.of(context).restoreBackupHint,
// clear button
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
settingsInputController.clear();
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return S.of(context).restoreBackupValidator;
}
try {
// try to decode the backup
settings.value = model.AppSettings.fromJson(
jsonDecode(value),
);
} catch (e) {
return S.of(context).restoreBackupInvalid;
}
return null;
},
),
),
actions: [
CancelButton(),
TextButton(
onPressed: () {
if (formKey.currentState!.validate()) {
if (settings.value == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context).restoreBackupInvalid),
),
);
return;
}
ref.read(appSettingsProvider.notifier).update(settings.value!);
settingsInputController.clear();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context).restoreBackupSuccess),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context).restoreBackupInvalid),
),
);
}
},
child: Text(S.of(context).restore),
),
],
);
}
}

View file

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/time_of_day.dart';
class AutoSleepTimerSettingsPage extends HookConsumerWidget {
const AutoSleepTimerSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final sleepTimerSettings = appSettings.sleepTimerSettings;
var enabled = sleepTimerSettings.autoTurnOnTimer &&
!sleepTimerSettings.alwaysAutoTurnOnTimer;
final selectedValueColor = enabled
? Theme.of(context).colorScheme.primary
: Theme.of(context).disabledColor;
return SimpleSettingsPage(
title: Text(S.of(context).autoSleepTimerSettings),
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
tiles: [
SettingsTile.switchTile(
// initialValue: sleepTimerSettings.autoTurnOnTimer,
title: Text(S.of(context).autoTurnOnTimer),
description: Text(
S.of(context).autoTurnOnTimerDescription,
),
leading: sleepTimerSettings.autoTurnOnTimer
? const Icon(Icons.timer_outlined)
: const Icon(Icons.timer_off_outlined),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings(
autoTurnOnTimer: value,
),
);
},
initialValue: sleepTimerSettings.autoTurnOnTimer,
),
// auto turn on time settings, enabled only when autoTurnOnTimer is enabled
SettingsTile.navigation(
enabled: enabled,
leading: const Icon(Icons.play_circle),
title: Text(S.of(context).autoTurnOnTimerFrom),
description: Text(
S.of(context).autoTurnOnTimerFromDescription,
),
onPressed: (context) async {
// navigate to the time picker
final selected = await showTimePicker(
context: context,
initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(),
);
if (selected != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings(
autoTurnOnTime: selected.toDuration(),
),
);
}
},
trailing: Text(
sleepTimerSettings.autoTurnOnTime.toTimeOfDay().format(context),
style: TextStyle(color: selectedValueColor),
),
),
SettingsTile.navigation(
enabled: enabled,
leading: const Icon(Icons.pause_circle),
title: Text(S.of(context).autoTurnOnTimerUntil),
description: Text(
S.of(context).autoTurnOnTimerUntilDescription,
),
onPressed: (context) async {
// navigate to the time picker
final selected = await showTimePicker(
context: context,
initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(),
);
if (selected != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings(
autoTurnOffTime: selected.toDuration(),
),
);
}
},
trailing: Text(
sleepTimerSettings.autoTurnOffTime
.toTimeOfDay()
.format(context),
style: TextStyle(color: selectedValueColor),
),
),
// switch tile for always auto turn on timer no matter what
SettingsTile.switchTile(
leading: const Icon(Icons.all_inclusive),
title: Text(S.of(context).autoTurnOnTimerAlways),
description: Text(
S.of(context).autoTurnOnTimerAlwaysDescription,
),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.sleepTimerSettings(
alwaysAutoTurnOnTimer: value,
),
);
},
enabled: sleepTimerSettings.autoTurnOnTimer,
initialValue: sleepTimerSettings.alwaysAutoTurnOnTimer,
),
],
),
],
);
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:vaani/generated/l10n.dart';
class OkButton<T> extends StatelessWidget {
const OkButton({
super.key,
this.onPressed,
});
final void Function()? onPressed;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Text(S.of(context).ok),
);
}
}
class CancelButton extends StatelessWidget {
const CancelButton({
super.key,
this.onPressed,
});
final void Function()? onPressed;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
onPressed?.call();
Navigator.of(context).pop();
},
child: Text(S.of(context).cancel),
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart'
show SimpleSettingsPage;
class HomePageSettingsPage extends HookConsumerWidget {
const HomePageSettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final appSettingsNotifier = ref.read(appSettingsProvider.notifier);
return SimpleSettingsPage(
title: Text(S.of(context).homePageSettings),
sections: [
SettingsSection(
title: Text(S.of(context).homePageSettingsQuickPlay),
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
tiles: [
SettingsTile.switchTile(
initialValue: appSettings
.homePageSettings.showPlayButtonOnContinueListeningShelf,
title: Text(S.of(context).homeContinueListening),
leading: const Icon(Icons.play_arrow),
description:
Text(S.of(context).homeBookContinueListeningDescription),
onToggle: (value) {
appSettingsNotifier.update(
appSettings.copyWith(
homePageSettings: appSettings.homePageSettings.copyWith(
showPlayButtonOnContinueListeningShelf: value,
),
),
);
},
),
SettingsTile.switchTile(
title: Text(S.of(context).homeBookContinueSeries),
leading: const Icon(Icons.play_arrow),
description:
Text(S.of(context).homeBookContinueSeriesDescription),
initialValue: appSettings
.homePageSettings.showPlayButtonOnContinueSeriesShelf,
onToggle: (value) {
appSettingsNotifier.update(
appSettings.copyWith(
homePageSettings: appSettings.homePageSettings.copyWith(
showPlayButtonOnContinueSeriesShelf: value,
),
),
);
},
),
SettingsTile.switchTile(
title: Text(S.of(context).homePageSettingsOtherShelves),
leading: const Icon(Icons.all_inclusive),
description:
Text(S.of(context).homePageSettingsOtherShelvesDescription),
initialValue: appSettings
.homePageSettings.showPlayButtonOnAllRemainingShelves,
onToggle: (value) {
appSettingsNotifier.update(
appSettings.copyWith(
homePageSettings: appSettings.homePageSettings.copyWith(
showPlayButtonOnAllRemainingShelves: value,
),
),
);
},
),
SettingsTile.switchTile(
title: Text(S.of(context).homeBookListenAgain),
leading: const Icon(Icons.replay),
description: Text(S.of(context).homeBookListenAgainDescription),
initialValue:
appSettings.homePageSettings.showPlayButtonOnListenAgainShelf,
onToggle: (value) {
appSettingsNotifier.update(
appSettings.copyWith(
homePageSettings: appSettings.homePageSettings.copyWith(
showPlayButtonOnListenAgainShelf: value,
),
),
);
},
),
],
),
],
);
}
}

View file

@ -0,0 +1,385 @@
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/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/enum.dart';
class NotificationSettingsPage extends HookConsumerWidget {
const NotificationSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final notificationSettings = appSettings.notificationSettings;
final primaryColor = Theme.of(context).colorScheme.primary;
return SimpleSettingsPage(
title: Text(S.of(context).notificationMediaPlayer),
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.only(
start: 16.0,
end: 16.0,
top: 8.0,
bottom: 8.0,
),
tiles: [
// set the primary and secondary titles
SettingsTile(
title: Text(S.of(context).nmpSettingsTitle),
description: Text.rich(
TextSpan(
text: S.of(context).nmpSettingsTitleDescription,
children: [
TextSpan(
text: notificationSettings.primaryTitle,
style: TextStyle(
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
],
),
),
leading: const Icon(Icons.title),
onPressed: (context) async {
// show the notification title picker
final selectedTitle = await showDialog<String>(
context: context,
builder: (context) {
return NotificationTitlePicker(
initialValue: notificationSettings.primaryTitle,
title: S.of(context).nmpSettingsTitle,
);
},
);
if (selectedTitle != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
primaryTitle: selectedTitle,
),
);
}
},
),
SettingsTile(
title: Text(S.of(context).nmpSettingsSubTitle),
description: Text.rich(
TextSpan(
text: S.of(context).nmpSettingsSubTitleDescription,
children: [
TextSpan(
text: notificationSettings.secondaryTitle,
style: TextStyle(
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
],
),
),
leading: const Icon(Icons.title),
onPressed: (context) async {
// show the notification title picker
final selectedTitle = await showDialog<String>(
context: context,
builder: (context) {
return NotificationTitlePicker(
initialValue: notificationSettings.secondaryTitle,
title: S.of(context).nmpSettingsSubTitle,
);
},
);
if (selectedTitle != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
secondaryTitle: selectedTitle,
),
);
}
},
),
// set forward and backward intervals
SettingsTile(
title: Text(S.of(context).nmpSettingsForward),
description: Row(
children: [
Text(
S.of(context).timeSecond(
notificationSettings.fastForwardInterval.inSeconds,
),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: notificationSettings.fastForwardInterval,
onChangedEnd: (interval) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
fastForwardInterval: interval,
),
);
},
),
),
],
),
leading: const Icon(Icons.fast_forward),
),
SettingsTile(
title: Text(S.of(context).nmpSettingsBackward),
description: Row(
children: [
Text(
S.of(context).timeSecond(
notificationSettings.rewindInterval.inSeconds,
),
),
Expanded(
child: TimeIntervalSlider(
defaultValue: notificationSettings.rewindInterval,
onChangedEnd: (interval) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
rewindInterval: interval,
),
);
},
),
),
],
),
leading: const Icon(Icons.fast_rewind),
),
// set the media controls
SettingsTile(
title: Text(S.of(context).nmpSettingsMediaControls),
leading: const Icon(Icons.control_camera),
// description: const Text('Select the media controls to display'),
description:
Text(S.of(context).nmpSettingsMediaControlsDescription),
trailing: Wrap(
spacing: 8.0,
children: notificationSettings.mediaControls
.map(
(control) => Icon(
control.icon,
color: primaryColor,
),
)
.toList(),
),
onPressed: (context) async {
final selectedControls =
await showDialog<List<NotificationMediaControl>>(
context: context,
builder: (context) {
return MediaControlsPicker(
selectedControls: notificationSettings.mediaControls,
);
},
);
if (selectedControls != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
mediaControls: selectedControls,
),
);
}
},
),
// set the progress bar to show chapter progress
SettingsTile.switchTile(
title: Text(S.of(context).nmpSettingsShowChapterProgress),
leading: const Icon(Icons.book),
description:
Text(S.of(context).nmpSettingsShowChapterProgressDescription),
initialValue: notificationSettings.progressBarIsChapterProgress,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.notificationSettings(
progressBarIsChapterProgress: value,
),
);
},
),
],
),
],
);
}
}
class MediaControlsPicker extends HookConsumerWidget {
const MediaControlsPicker({
super.key,
required this.selectedControls,
});
final List<NotificationMediaControl> selectedControls;
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedMediaControls = useState(selectedControls);
return AlertDialog(
title: Text(S.of(context).nmpSettingsMediaControls),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(selectedMediaControls.value);
},
),
],
// a list of chips to easily select the media controls to display
// with icons and labels
content: Wrap(
spacing: 8.0,
children: NotificationMediaControl.values
.map(
(control) => ChoiceChip(
avatar: Icon(control.icon),
label: Text(control.pascalCase),
selected: selectedMediaControls.value.contains(control),
onSelected: (selected) {
if (selected) {
selectedMediaControls.value = [
...selectedMediaControls.value,
control,
];
} else {
selectedMediaControls.value = [
...selectedMediaControls.value.where((c) => c != control),
];
}
},
),
)
.toList(),
),
);
}
}
class TimeIntervalSlider extends HookConsumerWidget {
const TimeIntervalSlider({
super.key,
this.title,
required this.defaultValue,
this.onChanged,
this.onChangedEnd,
this.min = const Duration(seconds: 5),
this.max = const Duration(seconds: 120),
this.step = const Duration(seconds: 5),
});
final Widget? title;
final Duration defaultValue;
final ValueChanged<Duration>? onChanged;
final ValueChanged<Duration>? onChangedEnd;
final Duration min;
final Duration max;
final Duration step;
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedInterval = useState(defaultValue);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
title ?? const SizedBox.shrink(),
if (title != null) const SizedBox(height: 8.0),
Slider(
value: selectedInterval.value.inSeconds.toDouble(),
min: min.inSeconds.toDouble(),
max: max.inSeconds.toDouble(),
divisions: ((max.inSeconds - min.inSeconds) ~/ step.inSeconds),
label: S.of(context).timeSecond(selectedInterval.value.inSeconds),
onChanged: (value) {
selectedInterval.value = Duration(seconds: value.toInt());
onChanged?.call(selectedInterval.value);
},
onChangeEnd: (value) {
onChangedEnd?.call(selectedInterval.value);
},
),
],
);
}
}
class NotificationTitlePicker extends HookConsumerWidget {
const NotificationTitlePicker({
super.key,
required this.initialValue,
required this.title,
});
final String initialValue;
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedTitle = useState(initialValue);
final controller = useTextEditingController(text: initialValue);
return AlertDialog(
title: Text(title),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(selectedTitle.value);
},
),
],
// a list of chips to easily insert available fields into the text field
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autofocus: true,
controller: controller,
onChanged: (value) {
selectedTitle.value = value;
},
decoration: InputDecoration(
helper: Text(S.of(context).nmpSettingsSelectOne),
suffix: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
controller.clear();
selectedTitle.value = '';
},
),
),
),
const SizedBox(height: 8.0),
Wrap(
spacing: 8.0,
children: NotificationTitleType.values
.map(
(type) => ActionChip(
label: Text(type.pascalCase),
onPressed: () {
final text = controller.text;
final newText = '$text\$${type.name}';
controller.text = newText;
selectedTitle.value = newText;
},
),
)
.toList(),
),
],
),
);
}
}

View file

@ -0,0 +1,440 @@
import 'package:duration_picker/duration_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
class PlayerSettingsPage extends HookConsumerWidget {
const PlayerSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final playerSettings = appSettings.playerSettings;
final primaryColor = Theme.of(context).colorScheme.primary;
return SimpleSettingsPage(
title: Text(S.of(context).playerSettings),
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
tiles: [
// preferred settings for every book
SettingsTile.switchTile(
title: Text(S.of(context).playerSettingsRememberForEveryBook),
leading: const Icon(Icons.settings_applications),
description: Text(
S.of(context).playerSettingsRememberForEveryBookDescription,
),
initialValue: playerSettings.configurePlayerForEveryBook,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
configurePlayerForEveryBook: value,
),
);
},
),
// preferred default speed
SettingsTile(
title: Text(S.of(context).playerSettingsSpeedDefault),
trailing: Text(
'${playerSettings.preferredDefaultSpeed}x',
style:
TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
),
leading: const Icon(Icons.speed),
onPressed: (context) async {
final newSpeed = await showDialog(
context: context,
builder: (context) => SpeedPicker(
initialValue: playerSettings.preferredDefaultSpeed,
),
);
if (newSpeed != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
preferredDefaultSpeed: newSpeed,
),
);
}
},
),
// preferred speed options
SettingsTile(
title: Text(S.of(context).playerSettingsSpeedOptions),
description: Text(
playerSettings.speedOptions.map((e) => '${e}x').join(', '),
style:
TextStyle(fontWeight: FontWeight.bold, color: primaryColor),
),
leading: const Icon(Icons.speed),
onPressed: (context) async {
final newSpeedOptions = await showDialog<List<double>?>(
context: context,
builder: (context) => SpeedOptionsPicker(
initialValue: playerSettings.speedOptions,
),
);
if (newSpeedOptions != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
speedOptions: newSpeedOptions..sort(),
),
);
}
},
),
],
),
// Playback Reporting
SettingsSection(
title: Text(S.of(context).playerSettingsPlaybackReporting),
tiles: [
SettingsTile(
title: Text(S.of(context).playerSettingsPlaybackReportingMinimum),
description: Text.rich(
TextSpan(
text: S
.of(context)
.playerSettingsPlaybackReportingMinimumDescriptionHead,
children: [
TextSpan(
text: playerSettings
.minimumPositionForReporting.smartBinaryFormat,
style: TextStyle(
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
TextSpan(
text: S
.of(context)
.playerSettingsPlaybackReportingMinimumDescriptionTail),
],
),
),
leading: const Icon(Icons.timer),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: Text(
S.of(context).playerSettingsPlaybackReportingIgnore),
baseUnit: BaseUnit.second,
initialValue: playerSettings.minimumPositionForReporting,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
minimumPositionForReporting: newDuration,
),
);
}
},
),
// when to mark complete
SettingsTile(
title: Text(S.of(context).playerSettingsCompleteTime),
description: Text.rich(
TextSpan(
text: S.of(context).playerSettingsCompleteTimeDescriptionHead,
children: [
TextSpan(
text: playerSettings
.markCompleteWhenTimeLeft.smartBinaryFormat,
style: TextStyle(
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
TextSpan(
text: S
.of(context)
.playerSettingsCompleteTimeDescriptionTail),
],
),
),
leading: const Icon(Icons.cloud_done),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: Text(S.of(context).playerSettingsCompleteTime),
baseUnit: BaseUnit.second,
initialValue: playerSettings.markCompleteWhenTimeLeft,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
markCompleteWhenTimeLeft: newDuration,
),
);
}
},
),
// playback report interval
SettingsTile(
title: Text(S.of(context).playerSettingsPlaybackInterval),
description: Text.rich(
TextSpan(
text: S
.of(context)
.playerSettingsPlaybackIntervalDescriptionHead,
children: [
TextSpan(
text: playerSettings
.playbackReportInterval.smartBinaryFormat,
style: TextStyle(
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
TextSpan(
text: S
.of(context)
.playerSettingsPlaybackIntervalDescriptionTail),
],
),
),
leading: const Icon(Icons.change_circle_outlined),
onPressed: (context) async {
final newDuration = await showDialog(
context: context,
builder: (context) {
return TimeDurationSelector(
title: Text(S.of(context).playerSettingsPlaybackInterval),
baseUnit: BaseUnit.second,
initialValue: playerSettings.playbackReportInterval,
);
},
);
if (newDuration != null) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
playbackReportInterval: newDuration,
),
);
}
},
),
],
),
// Display Settings
SettingsSection(
title: Text(S.of(context).playerSettingsDisplay),
tiles: [
// show total progress
SettingsTile.switchTile(
title: Text(S.of(context).playerSettingsDisplayTotalProgress),
leading: const Icon(Icons.show_chart),
description: Text(
S.of(context).playerSettingsDisplayTotalProgressDescription,
),
initialValue:
playerSettings.expandedPlayerSettings.showTotalProgress,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings
.expandedPlayerSettings(showTotalProgress: value),
);
},
),
// show chapter progress
SettingsTile.switchTile(
title: Text(S.of(context).playerSettingsDisplayChapterProgress),
leading: const Icon(Icons.show_chart),
description: Text(
S.of(context).playerSettingsDisplayChapterProgressDescription,
),
initialValue:
playerSettings.expandedPlayerSettings.showChapterProgress,
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.playerSettings(
expandedPlayerSettings: playerSettings
.expandedPlayerSettings
.copyWith(showChapterProgress: value),
),
);
},
),
],
),
],
);
}
}
class TimeDurationSelector extends HookConsumerWidget {
const TimeDurationSelector({
super.key,
this.title = const Text('Select Duration'),
this.baseUnit = BaseUnit.second,
this.initialValue = Duration.zero,
});
final Widget title;
final BaseUnit baseUnit;
final Duration initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final duration = useState(initialValue);
return AlertDialog(
title: title,
content: DurationPicker(
duration: duration.value,
baseUnit: baseUnit,
onChange: (value) {
duration.value = value;
},
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(duration.value);
},
),
],
);
}
}
class SpeedPicker extends HookConsumerWidget {
const SpeedPicker({
super.key,
this.initialValue = 1,
});
final double initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final speedController =
useTextEditingController(text: initialValue.toString());
final speed = useState<double?>(initialValue);
return AlertDialog(
title: Text(S.of(context).playerSettingsSpeedSelect),
content: TextField(
controller: speedController,
onChanged: (value) => speed.value = double.tryParse(value),
onSubmitted: (value) {
Navigator.of(context).pop(speed.value);
},
autofocus: true,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: S.of(context).playerSettingsSpeed,
helper: Text(S.of(context).playerSettingsSpeedSelectHelper),
),
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(speed.value);
},
),
],
);
}
}
class SpeedOptionsPicker extends HookConsumerWidget {
const SpeedOptionsPicker({
super.key,
this.initialValue = const [0.75, 1, 1.25, 1.5, 1.75, 2],
});
final List<double> initialValue;
@override
Widget build(BuildContext context, WidgetRef ref) {
final speedOptionAddController = useTextEditingController();
final speedOptions = useState<List<double>>(initialValue);
final focusNode = useFocusNode();
return AlertDialog(
title: Text(S.of(context).playerSettingsSpeedOptionsSelect),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: speedOptions.value
.map(
(speed) => Chip(
label: Text('${speed}x'),
onDeleted: speed == 1
? null
: () {
speedOptions.value =
speedOptions.value.where((element) {
// speed option 1 can't be removed
return element != speed;
}).toList();
},
),
)
.toList()
..sort((a, b) {
// if (a.label == const Text('1x')) {
// return -1;
// } else if (b.label == const Text('1x')) {
// return 1;
// }
return a.label.toString().compareTo(b.label.toString());
}),
),
TextField(
focusNode: focusNode,
autofocus: true,
controller: speedOptionAddController,
onSubmitted: (value) {
final newSpeed = double.tryParse(value);
if (newSpeed != null && !speedOptions.value.contains(newSpeed)) {
speedOptions.value = [...speedOptions.value, newSpeed];
}
speedOptionAddController.clear();
focusNode.requestFocus();
},
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: S.of(context).playerSettingsSpeedOptionsSelectAdd,
helper:
Text(S.of(context).playerSettingsSpeedOptionsSelectAddHelper),
),
),
],
),
actions: [
const CancelButton(),
OkButton(
onPressed: () {
Navigator.of(context).pop(speedOptions.value);
},
),
],
);
}
}

View file

@ -0,0 +1,396 @@
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/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vaani/features/settings/view/buttons.dart';
import 'package:vaani/features/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: Text(S.of(context).shakeDetectorSettings),
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: Text(S.of(context).shakeDetectorEnable),
description: Text(
S.of(context).shakeDetectorEnableDescription,
),
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: Text(S.of(context).shakeActivationThreshold),
description: Text(
S.of(context).shakeActivationThresholdDescription,
),
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: Text(S.of(context).shakeAction),
description: Text(
S.of(context).shakeActionDescription,
),
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: Text(S.of(context).shakeFeedback),
description: Text(
S.of(context).shakeFeedbackDescription,
),
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: Text(S.of(context).shakeSelectFeedback),
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: Text(S.of(context).shakeSelectAction),
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: Text(S.of(context).shakeSelectActivationThreshold),
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: Text(
S.of(context).shakeSelectActivationThresholdHelper,
),
),
),
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';
}
}
}

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
class SimpleSettingsPage extends HookConsumerWidget {
const SimpleSettingsPage({
super.key,
this.title,
this.sections,
});
final Widget? title;
final List<AbstractSettingsSection>? sections;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
// appBar: AppBar(
// title: title,
// ),
// body: body,
// an app bar which is bigger than the default app bar but on scroll shrinks to the default app bar with the title being animated
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: title,
// background: Theme.of(context).primaryColor,
),
),
if (sections != null)
SliverList(
delegate: SliverChildListDelegate(
[
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20)),
child: SettingsList(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
sections: sections!,
),
),
],
),
),
// some padding at the bottom
const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
],
),
);
}
}

View file

@ -0,0 +1,203 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_settings_ui/flutter_settings_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/generated/l10n.dart';
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/view/simple_settings_page.dart';
class ThemeSettingsPage extends HookConsumerWidget {
const ThemeSettingsPage({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final themeSettings = appSettings.themeSettings;
// final primaryColor = Theme.of(context).colorScheme.primary;
return SimpleSettingsPage(
title: Text(S.of(context).themeSettings),
sections: [
SettingsSection(
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
tiles: [
// choose system , light or dark theme
SettingsTile(
title: Text(S.of(context).themeMode),
description: SegmentedButton(
expandedInsets: const EdgeInsets.only(top: 8.0),
showSelectedIcon: true,
selectedIcon: const Icon(Icons.check),
selected: {themeSettings.themeMode},
onSelectionChanged: (newSelection) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.themeSettings(
themeMode: newSelection.first,
),
);
},
segments: [
ButtonSegment(
value: ThemeMode.light,
icon: Icon(Icons.light_mode),
label: Text(S.of(context).themeModeLight),
),
ButtonSegment(
value: ThemeMode.system,
icon: Icon(Icons.auto_awesome),
label: Text(S.of(context).themeModeSystem),
),
ButtonSegment(
value: ThemeMode.dark,
icon: Icon(Icons.dark_mode),
label: Text(S.of(context).themeModeDark),
),
],
),
leading: Icon(
themeSettings.themeMode == ThemeMode.light
? Icons.light_mode
: themeSettings.themeMode == ThemeMode.dark
? Icons.dark_mode
: Icons.auto_awesome,
),
),
// high contrast mode
SettingsTile.switchTile(
leading: themeSettings.highContrast
? const Icon(Icons.accessibility)
: const Icon(Icons.accessibility_new_outlined),
initialValue: themeSettings.highContrast,
title: Text(S.of(context).themeModeHighContrast),
description: Text(
S.of(context).themeModeHighContrastDescription,
),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.themeSettings(
highContrast: value,
),
);
},
),
// use material theme from system
SettingsTile.switchTile(
initialValue: themeSettings.useMaterialThemeFromSystem,
title: Platform.isAndroid
? Text(S.of(context).themeSettingsColorsAndroid)
: Text(S.of(context).themeSettingsColors),
description: Text(S.of(context).themeSettingsColorsDescription),
leading: themeSettings.useMaterialThemeFromSystem
? const Icon(Icons.auto_awesome)
: const Icon(Icons.auto_fix_off),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.themeSettings(
useMaterialThemeFromSystem: value,
),
);
},
),
// TODO choose the primary color
// SettingsTile.navigation(
// title: const Text('Primary Color'),
// description: const Text(
// 'Choose the primary color for the app',
// ),
// leading: const Icon(Icons.colorize),
// trailing: Icon(
// Icons.circle,
// color: themeSettings.customThemeColor.toColor(),
// ),
// onPressed: (context) async {
// final selectedColor = await showDialog<Color>(
// context: context,
// builder: (context) {
// return SimpleDialog(
// title: const Text('Select Primary Color'),
// children: [
// for (final color in Colors.primaries)
// SimpleDialogOption(
// onPressed: () {
// Navigator.pop(context, color);
// },
// child: Container(
// color: color,
// height: 48,
// ),
// ),
// ],
// );
// },
// );
// if (selectedColor != null) {
// ref.read(appSettingsProvider.notifier).update(
// appSettings.copyWith.themeSettings(
// customThemeColor: selectedColor.toHexString(),
// ),
// );
// }
// },
// ),
// use theme throughout the app when playing item
SettingsTile.switchTile(
initialValue: themeSettings.useCurrentPlayerThemeThroughoutApp,
title: Text(S.of(context).themeSettingsColorsCurrent),
description:
Text(S.of(context).themeSettingsColorsCurrentDescription),
leading: themeSettings.useCurrentPlayerThemeThroughoutApp
? const Icon(Icons.auto_fix_high)
: const Icon(Icons.auto_fix_off),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.themeSettings(
useCurrentPlayerThemeThroughoutApp: value,
),
);
},
),
SettingsTile.switchTile(
initialValue: themeSettings.useMaterialThemeOnItemPage,
title: Text(S.of(context).themeSettingsColorsBook),
description:
Text(S.of(context).themeSettingsColorsBookDescription),
leading: themeSettings.useMaterialThemeOnItemPage
? const Icon(Icons.auto_fix_high)
: const Icon(Icons.auto_fix_off),
onToggle: (value) {
ref.read(appSettingsProvider.notifier).update(
appSettings.copyWith.themeSettings(
useMaterialThemeOnItemPage: value,
),
);
},
),
],
),
],
);
}
}
extension ColorExtension on Color {
String toHexString() {
return '#${value.toRadixString(16).substring(2)}';
}
}
extension StringExtension on String {
Color toColor() {
return Color(int.parse('0xff$substring(1)'));
}
}

View 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.withValues(alpha: 0.5),
indent: 8.0,
endIndent: 8.0,
),
Switch.adaptive(
value: value,
onChanged: onToggle,
),
],
),
),
);
}
}