mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-15 21:03:41 +00:00
refactor(ui): refine advanced options and settings feedback (#1661)
This commit is contained in:
30 files changed
+243
-68
No files matched your search
@@ -19,6 +19,12 @@ class SettingsState {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsSaveResult {
|
||||
const SettingsSaveResult({required this.btRestartRequired});
|
||||
|
||||
final bool btRestartRequired;
|
||||
}
|
||||
|
||||
class SettingsController extends AsyncNotifier<SettingsState> {
|
||||
Future<void> _saveTail = Future<void>.value();
|
||||
int _saveRevision = 0;
|
||||
@@ -36,7 +42,7 @@ class SettingsController extends AsyncNotifier<SettingsState> {
|
||||
state = AsyncValue.data(SettingsState(config: config));
|
||||
}
|
||||
|
||||
Future<void> save(DownloaderConfig config) {
|
||||
Future<SettingsSaveResult> save(DownloaderConfig config) {
|
||||
final snapshot = DownloaderConfig.fromJson(config.toJson());
|
||||
final revision = ++_saveRevision;
|
||||
state = AsyncValue.data(SettingsState(config: config, saving: true));
|
||||
@@ -47,10 +53,15 @@ class SettingsController extends AsyncNotifier<SettingsState> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _save(DownloaderConfig config, int revision) async {
|
||||
Future<SettingsSaveResult> _save(DownloaderConfig config, int revision) async {
|
||||
try {
|
||||
final service = ref.read(gopeedServiceProvider);
|
||||
final latest = await service.getConfig();
|
||||
final previousBt = (
|
||||
latest.protocolConfig.bt.listenPort,
|
||||
latest.protocolConfig.bt.disableDht,
|
||||
latest.protocolConfig.bt.disablePex,
|
||||
);
|
||||
config.extra
|
||||
..windowState = latest.extra.windowState
|
||||
..bookmarks = latest.extra.bookmarks
|
||||
@@ -64,6 +75,12 @@ class SettingsController extends AsyncNotifier<SettingsState> {
|
||||
ref.read(appRuntimeControllerProvider.notifier).replaceDownloaderConfig(saved);
|
||||
if (revision == _saveRevision) state = AsyncValue.data(SettingsState(config: saved));
|
||||
}
|
||||
final savedBt = (
|
||||
saved.protocolConfig.bt.listenPort,
|
||||
saved.protocolConfig.bt.disableDht,
|
||||
saved.protocolConfig.bt.disablePex,
|
||||
);
|
||||
return SettingsSaveResult(btRestartRequired: previousBt != savedBt);
|
||||
} finally {
|
||||
if (ref.mounted && revision == _saveRevision && state.value?.saving == true) {
|
||||
state = AsyncValue.data(state.requireValue.copyWith(saving: false));
|
||||
|
||||
@@ -625,7 +625,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
),
|
||||
SettingsItem(
|
||||
title: context.l10n.enableDht,
|
||||
subtitle: context.l10n.restartRequired,
|
||||
child: shad.Switch(
|
||||
value: !config.protocolConfig.bt.disableDht,
|
||||
onChanged: (value) => _mutateConfig((next) => next.protocolConfig.bt.disableDht = !value),
|
||||
@@ -633,7 +632,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
),
|
||||
SettingsItem(
|
||||
title: context.l10n.enablePex,
|
||||
subtitle: context.l10n.restartRequired,
|
||||
child: shad.Switch(
|
||||
value: !config.protocolConfig.bt.disablePex,
|
||||
onChanged: (value) => _mutateConfig((next) => next.protocolConfig.bt.disablePex = !value),
|
||||
@@ -1204,7 +1202,10 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
final snapshot = DownloaderConfig.fromJson(config.toJson());
|
||||
_loadedSignature = _signature(snapshot);
|
||||
try {
|
||||
await ref.read(settingsControllerProvider.notifier).save(snapshot);
|
||||
final result = await ref.read(settingsControllerProvider.notifier).save(snapshot);
|
||||
if (mounted && result.btRestartRequired) {
|
||||
_toast(context.l10n.restartRequired, type: AppToastType.info);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (mounted) _toast(error.toString());
|
||||
|
||||
@@ -41,6 +41,7 @@ import '../../application/pending_download.dart';
|
||||
import '../../application/task_list_navigation.dart';
|
||||
import '../widgets/history_search_field.dart';
|
||||
import '../widgets/create_task_action_buttons.dart';
|
||||
import '../widgets/create_task_advanced_toggle.dart';
|
||||
import '../widgets/resolve_download_dialog.dart';
|
||||
|
||||
class CreateTaskWindowPage extends ConsumerStatefulWidget {
|
||||
@@ -560,20 +561,11 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: GestureDetector(
|
||||
onTap: _toggleAdvanced,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.advanced,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_TripleChevronIcon(expanded: _showAdvanced, color: palette.textSecondary),
|
||||
],
|
||||
),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: CreateTaskAdvancedToggle(
|
||||
key: const ValueKey('create-task-advanced-toggle'),
|
||||
expanded: _showAdvanced,
|
||||
onPressed: _toggleAdvanced,
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
@@ -1933,30 +1925,6 @@ class _MiniInputAction extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TripleChevronIcon extends StatelessWidget {
|
||||
const _TripleChevronIcon({required this.expanded, required this.color});
|
||||
|
||||
final bool expanded;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = expanded ? Icons.expand_less : Icons.expand_more;
|
||||
return SizedBox(
|
||||
width: 14,
|
||||
height: 18,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(top: 0, left: 0, child: Icon(icon, size: 14, color: color)),
|
||||
Positioned(top: 4, left: 0, child: Icon(icon, size: 14, color: color)),
|
||||
Positioned(top: 8, left: 0, child: Icon(icon, size: 14, color: color)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SegmentButton extends StatelessWidget {
|
||||
const _SegmentButton({required this.label, required this.active, required this.onTap});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../../../../l10n/l10n.dart';
|
||||
import '../../../../shared/theme/app_design_tokens.dart';
|
||||
import '../../../../shared/theme/app_palette.dart';
|
||||
|
||||
/// A quiet disclosure control with the same keyboard behavior as other buttons.
|
||||
class CreateTaskAdvancedToggle extends StatelessWidget {
|
||||
const CreateTaskAdvancedToggle({super.key, required this.expanded, required this.onPressed});
|
||||
|
||||
final bool expanded;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
final reduceMotion = MediaQuery.disableAnimationsOf(context);
|
||||
return Semantics(
|
||||
expanded: expanded,
|
||||
child: ButtonStyleOverride.inherit(
|
||||
padding: (context, states, value) => const EdgeInsets.symmetric(vertical: AppDesignTokens.space12),
|
||||
child: GhostButton(
|
||||
onPressed: onPressed,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 20),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
context.l10n.advanced,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
AnimatedRotation(
|
||||
turns: expanded ? 0.5 : 0,
|
||||
duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(Icons.keyboard_arrow_down, size: 16, color: palette.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user