This commit is contained in:
rang 2025-12-29 17:56:03 +08:00
parent 6ffd76a194
commit b0f5dd8951
18 changed files with 441 additions and 64 deletions

View file

@ -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) =>

View file

@ -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.

View file

@ -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(

View file

@ -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),

View 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);
},
),
],
);
}
}