mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2026-02-16 06:19:35 +00:00
123
This commit is contained in:
parent
6ffd76a194
commit
b0f5dd8951
18 changed files with 441 additions and 64 deletions
|
|
@ -22,6 +22,7 @@ class AudiobookDownloadManager {
|
|||
this.requiresWiFi = true,
|
||||
this.retries = 0,
|
||||
this.allowPause = false,
|
||||
this.path = '',
|
||||
|
||||
// /// The maximum number of concurrent tasks to run at any given time.
|
||||
// int maxConcurrent = 3,
|
||||
|
|
@ -60,6 +61,9 @@ class AudiobookDownloadManager {
|
|||
// whether to allow pausing of downloads
|
||||
final bool allowPause;
|
||||
|
||||
// 下载目录
|
||||
final String path;
|
||||
|
||||
final StreamController<TaskUpdate> _taskStatusController =
|
||||
StreamController.broadcast();
|
||||
|
||||
|
|
@ -68,11 +72,16 @@ class AudiobookDownloadManager {
|
|||
late StreamSubscription<TaskUpdate> _updatesSubscription;
|
||||
|
||||
Future<void> queueAudioBookDownload(
|
||||
LibraryItemExpanded item,
|
||||
) async {
|
||||
LibraryItemExpanded item, {
|
||||
String prePath = '',
|
||||
}) async {
|
||||
_logger.info('queuing download for item: ${item.id}');
|
||||
// create a download task for each file in the item
|
||||
for (final file in item.libraryFiles) {
|
||||
// 仅下载音频和视频
|
||||
if (![FileType.audio, FileType.video].contains(file.fileType)) {
|
||||
continue;
|
||||
}
|
||||
// check if the file is already downloaded
|
||||
if (isFileDownloaded(
|
||||
constructFilePath(item, file),
|
||||
|
|
@ -84,7 +93,7 @@ class AudiobookDownloadManager {
|
|||
final task = DownloadTask(
|
||||
taskId: file.ino,
|
||||
url: file.url(baseUrl, item.id, token).toString(),
|
||||
directory: item.relPath,
|
||||
directory: prePath + item.relPath,
|
||||
filename: file.metadata.filename,
|
||||
requiresWiFi: requiresWiFi,
|
||||
retries: retries,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -26,6 +28,7 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
|
|||
requiresWiFi: downloadSettings.requiresWiFi,
|
||||
retries: downloadSettings.retries,
|
||||
allowPause: downloadSettings.allowPause,
|
||||
path: downloadSettings.path,
|
||||
);
|
||||
core.tq.maxConcurrent = downloadSettings.maxConcurrent;
|
||||
core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost;
|
||||
|
|
@ -56,6 +59,8 @@ class DownloadManager extends _$DownloadManager {
|
|||
LibraryItemExpanded item,
|
||||
) async {
|
||||
_logger.fine('queueing download for ${item.id}');
|
||||
// final appSettings = ref.read(appSettingsProvider);
|
||||
|
||||
await state.queueAudioBookDownload(
|
||||
item,
|
||||
);
|
||||
|
|
@ -66,6 +71,24 @@ class DownloadManager extends _$DownloadManager {
|
|||
await state.deleteDownloadedItem(item);
|
||||
ref.notifyListeners();
|
||||
}
|
||||
|
||||
String _getDirectory(String path) {
|
||||
if (Platform.isWindows) {
|
||||
return path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
BaseDirectory _getBaseDirectory() {
|
||||
if (Platform.isIOS) {
|
||||
return BaseDirectory.applicationDocuments;
|
||||
} else if (Platform.isAndroid) {
|
||||
return BaseDirectory.temporary;
|
||||
} else if (Platform.isWindows) {
|
||||
return BaseDirectory.root;
|
||||
}
|
||||
return BaseDirectory.applicationSupport;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class _DownloadHistoryProviderElement
|
|||
}
|
||||
|
||||
String _$simpleDownloadManagerHash() =>
|
||||
r'8ab13f06ec5f2f73b73064bd285813dc890b7f36';
|
||||
r'da5798e4becce751db80c41b93a48217418e4648';
|
||||
|
||||
/// See also [SimpleDownloadManager].
|
||||
@ProviderFor(SimpleDownloadManager)
|
||||
|
|
@ -176,7 +176,7 @@ final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
|
|||
);
|
||||
|
||||
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
||||
String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982';
|
||||
String _$downloadManagerHash() => r'92afe484d6735d5de53473011ea9ecbad107fc1c';
|
||||
|
||||
/// See also [DownloadManager].
|
||||
@ProviderFor(DownloadManager)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class LibraryItemActions extends HookConsumerWidget {
|
|||
},
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
),
|
||||
LibItemDownButton(item.id),
|
||||
LibItemDownButton(item: item),
|
||||
// download button
|
||||
LibItemDownloadButton(item: item),
|
||||
|
||||
|
|
@ -205,8 +205,8 @@ class LibraryItemActions extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
class LibItemDownButton extends HookConsumerWidget {
|
||||
const LibItemDownButton(this.libraryItemId, {super.key});
|
||||
final String libraryItemId;
|
||||
const LibItemDownButton({required this.item, super.key});
|
||||
final shelfsdk.LibraryItemExpanded item;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
|
|
@ -217,7 +217,7 @@ class LibItemDownButton extends HookConsumerWidget {
|
|||
builder: (context) {
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 0.8,
|
||||
child: LibItemDownSheet(libraryItemId),
|
||||
child: LibItemDownSheet(item.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -253,13 +253,17 @@ class LibItemDownSheet extends HookConsumerWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
appLogger.fine('Pressed delete download button');
|
||||
ref
|
||||
.read(downloadManagerProvider.notifier)
|
||||
.deleteDownloadedItem(
|
||||
item,
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.delete_outlined),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.download_outlined),
|
||||
),
|
||||
LibItemDownloadButton(item: item),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ final playerStateProvider =
|
|||
);
|
||||
|
||||
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
|
||||
String _$currentBookHash() => r'b4f6b6ccc772631db3dfd9070be3d7487333544d';
|
||||
String _$currentBookHash() => r'790af1f9502b12879fc22c900ed5e3572381ab1e';
|
||||
|
||||
/// See also [CurrentBook].
|
||||
@ProviderFor(CurrentBook)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
child: ChapterSelectionModal(),
|
||||
child: ChapterSelectionModal(
|
||||
back: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ class ChapterSelectionButton extends HookConsumerWidget {
|
|||
class ChapterSelectionModal extends HookConsumerWidget {
|
||||
const ChapterSelectionModal({
|
||||
super.key,
|
||||
this.back = true,
|
||||
});
|
||||
final bool back;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -108,8 +110,11 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
|||
: const Icon(Icons.play_arrow),
|
||||
selected: isCurrent,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
||||
if (back) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ref.read(absPlayerProvider).switchChapter(chapter.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ class DownloadSettings with _$DownloadSettings {
|
|||
@Default(3) int maxConcurrent,
|
||||
@Default(3) int maxConcurrentByHost,
|
||||
@Default(3) int maxConcurrentByGroup,
|
||||
@Default('') String path,
|
||||
}) = _DownloadSettings;
|
||||
|
||||
factory DownloadSettings.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -1970,6 +1970,7 @@ mixin _$DownloadSettings {
|
|||
int get maxConcurrent => throw _privateConstructorUsedError;
|
||||
int get maxConcurrentByHost => throw _privateConstructorUsedError;
|
||||
int get maxConcurrentByGroup => throw _privateConstructorUsedError;
|
||||
String get path => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this DownloadSettings to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
|
@ -1993,7 +1994,8 @@ abstract class $DownloadSettingsCopyWith<$Res> {
|
|||
bool allowPause,
|
||||
int maxConcurrent,
|
||||
int maxConcurrentByHost,
|
||||
int maxConcurrentByGroup});
|
||||
int maxConcurrentByGroup,
|
||||
String path});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
|
@ -2017,6 +2019,7 @@ class _$DownloadSettingsCopyWithImpl<$Res, $Val extends DownloadSettings>
|
|||
Object? maxConcurrent = null,
|
||||
Object? maxConcurrentByHost = null,
|
||||
Object? maxConcurrentByGroup = null,
|
||||
Object? path = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
requiresWiFi: null == requiresWiFi
|
||||
|
|
@ -2043,6 +2046,10 @@ class _$DownloadSettingsCopyWithImpl<$Res, $Val extends DownloadSettings>
|
|||
? _value.maxConcurrentByGroup
|
||||
: maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
path: null == path
|
||||
? _value.path
|
||||
: path // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
|
@ -2061,7 +2068,8 @@ abstract class _$$DownloadSettingsImplCopyWith<$Res>
|
|||
bool allowPause,
|
||||
int maxConcurrent,
|
||||
int maxConcurrentByHost,
|
||||
int maxConcurrentByGroup});
|
||||
int maxConcurrentByGroup,
|
||||
String path});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
|
@ -2083,6 +2091,7 @@ class __$$DownloadSettingsImplCopyWithImpl<$Res>
|
|||
Object? maxConcurrent = null,
|
||||
Object? maxConcurrentByHost = null,
|
||||
Object? maxConcurrentByGroup = null,
|
||||
Object? path = null,
|
||||
}) {
|
||||
return _then(_$DownloadSettingsImpl(
|
||||
requiresWiFi: null == requiresWiFi
|
||||
|
|
@ -2109,6 +2118,10 @@ class __$$DownloadSettingsImplCopyWithImpl<$Res>
|
|||
? _value.maxConcurrentByGroup
|
||||
: maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
path: null == path
|
||||
? _value.path
|
||||
: path // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -2122,7 +2135,8 @@ class _$DownloadSettingsImpl implements _DownloadSettings {
|
|||
this.allowPause = true,
|
||||
this.maxConcurrent = 3,
|
||||
this.maxConcurrentByHost = 3,
|
||||
this.maxConcurrentByGroup = 3});
|
||||
this.maxConcurrentByGroup = 3,
|
||||
this.path = ''});
|
||||
|
||||
factory _$DownloadSettingsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$DownloadSettingsImplFromJson(json);
|
||||
|
|
@ -2145,10 +2159,13 @@ class _$DownloadSettingsImpl implements _DownloadSettings {
|
|||
@override
|
||||
@JsonKey()
|
||||
final int maxConcurrentByGroup;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String path;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DownloadSettings(requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause, maxConcurrent: $maxConcurrent, maxConcurrentByHost: $maxConcurrentByHost, maxConcurrentByGroup: $maxConcurrentByGroup)';
|
||||
return 'DownloadSettings(requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause, maxConcurrent: $maxConcurrent, maxConcurrentByHost: $maxConcurrentByHost, maxConcurrentByGroup: $maxConcurrentByGroup, path: $path)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -2166,13 +2183,21 @@ class _$DownloadSettingsImpl implements _DownloadSettings {
|
|||
(identical(other.maxConcurrentByHost, maxConcurrentByHost) ||
|
||||
other.maxConcurrentByHost == maxConcurrentByHost) &&
|
||||
(identical(other.maxConcurrentByGroup, maxConcurrentByGroup) ||
|
||||
other.maxConcurrentByGroup == maxConcurrentByGroup));
|
||||
other.maxConcurrentByGroup == maxConcurrentByGroup) &&
|
||||
(identical(other.path, path) || other.path == path));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, requiresWiFi, retries,
|
||||
allowPause, maxConcurrent, maxConcurrentByHost, maxConcurrentByGroup);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
requiresWiFi,
|
||||
retries,
|
||||
allowPause,
|
||||
maxConcurrent,
|
||||
maxConcurrentByHost,
|
||||
maxConcurrentByGroup,
|
||||
path);
|
||||
|
||||
/// Create a copy of DownloadSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
|
@ -2198,7 +2223,8 @@ abstract class _DownloadSettings implements DownloadSettings {
|
|||
final bool allowPause,
|
||||
final int maxConcurrent,
|
||||
final int maxConcurrentByHost,
|
||||
final int maxConcurrentByGroup}) = _$DownloadSettingsImpl;
|
||||
final int maxConcurrentByGroup,
|
||||
final String path}) = _$DownloadSettingsImpl;
|
||||
|
||||
factory _DownloadSettings.fromJson(Map<String, dynamic> json) =
|
||||
_$DownloadSettingsImpl.fromJson;
|
||||
|
|
@ -2215,6 +2241,8 @@ abstract class _DownloadSettings implements DownloadSettings {
|
|||
int get maxConcurrentByHost;
|
||||
@override
|
||||
int get maxConcurrentByGroup;
|
||||
@override
|
||||
String get path;
|
||||
|
||||
/// Create a copy of DownloadSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ _$DownloadSettingsImpl _$$DownloadSettingsImplFromJson(
|
|||
maxConcurrentByHost: (json['maxConcurrentByHost'] as num?)?.toInt() ?? 3,
|
||||
maxConcurrentByGroup:
|
||||
(json['maxConcurrentByGroup'] as num?)?.toInt() ?? 3,
|
||||
path: json['path'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DownloadSettingsImplToJson(
|
||||
|
|
@ -250,6 +251,7 @@ Map<String, dynamic> _$$DownloadSettingsImplToJson(
|
|||
'maxConcurrent': instance.maxConcurrent,
|
||||
'maxConcurrentByHost': instance.maxConcurrentByHost,
|
||||
'maxConcurrentByGroup': instance.maxConcurrentByGroup,
|
||||
'path': instance.path,
|
||||
};
|
||||
|
||||
_$NotificationSettingsImpl _$$NotificationSettingsImplFromJson(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class AppSettingsPage extends HookConsumerWidget {
|
|||
tiles: [
|
||||
SettingsTile(
|
||||
title: Text(S.of(context).language),
|
||||
leading: const Icon(Icons.play_arrow),
|
||||
leading: const Icon(Icons.language),
|
||||
trailing: DropdownButton(
|
||||
value: appSettings.language,
|
||||
items: S.delegate.supportedLocales.map((locale) {
|
||||
|
|
@ -66,6 +66,14 @@ class AppSettingsPage extends HookConsumerWidget {
|
|||
context.pushNamed(Routes.playerSettings.name);
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text('下载设置'),
|
||||
leading: const Icon(Icons.download),
|
||||
description: Text('自定义下载设置'),
|
||||
onPressed: (context) {
|
||||
context.pushNamed(Routes.downloadSettings.name);
|
||||
},
|
||||
),
|
||||
NavigationWithSwitchTile(
|
||||
title: Text(S.of(context).autoTurnOnSleepTimer),
|
||||
description: Text(S.of(context).automaticallyDescription),
|
||||
|
|
|
|||
239
lib/features/settings/view/download_settings_page.dart
Normal file
239
lib/features/settings/view/download_settings_page.dart
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:duration_picker/duration_picker.dart';
|
||||
import 'package:file_picker/file_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/globals.dart';
|
||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||
|
||||
class DownloadSettingsPage extends HookConsumerWidget {
|
||||
const DownloadSettingsPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettings = ref.watch(appSettingsProvider);
|
||||
final downloadSettings = appSettings.downloadSettings;
|
||||
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
|
||||
if (Platform.isWindows)
|
||||
SettingsTile(
|
||||
title: Text('下载目录'),
|
||||
leading: const Icon(Icons.folder_open_rounded),
|
||||
description: Text(
|
||||
'自定义下载目录',
|
||||
),
|
||||
trailing: Text(downloadSettings.path),
|
||||
onPressed: (context) async {
|
||||
String? selectedDirectory =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
appLogger.info(selectedDirectory);
|
||||
if (selectedDirectory != null) {
|
||||
ref.read(appSettingsProvider.notifier).update(
|
||||
appSettings.copyWith.downloadSettings(
|
||||
path: selectedDirectory,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text('并发数'),
|
||||
leading: const Icon(Icons.folder_open_rounded),
|
||||
description: Text('下载任务并发数'),
|
||||
trailing: Text('${downloadSettings.maxConcurrent}'),
|
||||
onPressed: (context) async {},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text('Host并发数'),
|
||||
leading: const Icon(Icons.folder_open_rounded),
|
||||
description: Text('同一Host并发连接数'),
|
||||
trailing: Text('${downloadSettings.maxConcurrentByHost}'),
|
||||
onPressed: (context) async {},
|
||||
),
|
||||
SettingsTile(
|
||||
title: Text('分组并发数'),
|
||||
leading: const Icon(Icons.folder_open_rounded),
|
||||
description: Text('同一分组并发数'),
|
||||
trailing: Text('${downloadSettings.maxConcurrentByGroup}'),
|
||||
onPressed: (context) async {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,11 @@ class Routes {
|
|||
name: 'playerSettings',
|
||||
parentRoute: settings,
|
||||
);
|
||||
static const downloadSettings = _SimpleRoute(
|
||||
pathName: 'downloadSettings',
|
||||
name: 'downloadSettings',
|
||||
parentRoute: settings,
|
||||
);
|
||||
static const shakeDetectorSettings = _SimpleRoute(
|
||||
pathName: 'shakeDetector',
|
||||
name: 'shakeDetectorSettings',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:vaani/features/onboarding/view/callback_page.dart';
|
|||
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart';
|
||||
import 'package:vaani/features/settings/view/app_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/auto_sleep_timer_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/download_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/home_page_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/notification_settings_page.dart';
|
||||
import 'package:vaani/features/settings/view/player_settings_page.dart';
|
||||
|
|
@ -213,6 +214,12 @@ class MyAppRouter {
|
|||
pageBuilder:
|
||||
defaultPageBuilder(const PlayerSettingsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.downloadSettings.pathName,
|
||||
name: Routes.downloadSettings.name,
|
||||
pageBuilder:
|
||||
defaultPageBuilder(const DownloadSettingsPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.shakeDetectorSettings.pathName,
|
||||
name: Routes.shakeDetectorSettings.name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue