Merge branch 'main' into codex/fix-settings-script-path-picker

This commit is contained in:
Levi authored and GitHub committed 2026-09-10 23:51:06 +08:00
commit d315f01289
52 files changed
+762 -144

No files matched your search

@@ -633,6 +633,7 @@ class _Toolbar extends StatelessWidget {
final search = AppTextField(
key: const ValueKey('extension-search-input'),
controller: searchController,
padding: MediaQuery.sizeOf(context).width >= Breakpoints.mobile ? AppDesignTokens.compactTextFieldPadding : null,
placeholder: Text(context.l10n.searchExtensions, style: TextStyle(color: palette.searchHint)),
features: [shad.InputFeature.leading(Icon(Icons.search_rounded, size: 16, color: palette.textMuted))],
onSubmitted: onSearch,
@@ -19,6 +19,7 @@ import '../../../../shared/widgets/responsive_navigation_layout.dart';
import '../../../../l10n/l10n.dart';
import '../../../../util/util.dart';
import '../../../tasks/application/tasks_controller.dart';
import '../../../tasks/application/task_list_navigation.dart';
import '../../../tasks/application/pending_update_task.dart';
import '../../../tasks/application/task_batch_selection_controller.dart';
import '../../../tasks/domain/task_record.dart';
@@ -86,6 +87,11 @@ class _HomePageState extends ConsumerState<HomePage> {
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final tasksAsync = ref.watch(tasksControllerProvider);
ref.listen(taskListNavigationProvider, (_, _) {
_searchController.clear();
_setActiveFilter(_TaskFilter.downloading);
setState(() => _batchMode = false);
});
final tasksState = tasksAsync.value;
final tasks = tasksState?.tasks ?? const <TaskRecord>[];
final filteredTasks = _filteredTasks(tasks);
@@ -0,0 +1,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
final taskListNavigationProvider = NotifierProvider<TaskListNavigation, int>(TaskListNavigation.new);
// A request also resets an already-visible list's filter, search and selection.
class TaskListNavigation extends Notifier<int> {
@override
int build() => 0;
void showDownloading() => state++;
}
@@ -29,9 +29,9 @@ import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_choice_segmented_control.dart';
import '../../../../shared/widgets/app_http_headers_editor.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import '../../../../shared/widgets/app_number_input.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/app_toast.dart';
@@ -39,6 +39,7 @@ import '../../../../l10n/l10n.dart';
import '../../../../util/util.dart';
import '../../application/pending_create_task.dart';
import '../widgets/resolve_file_tree.dart';
import '../widgets/resolve_task_actions.dart';
class CreateTaskWindowPage extends ConsumerStatefulWidget {
const CreateTaskWindowPage({super.key, this.windowController, this.initialTask});
@@ -87,6 +88,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
String _configuredDownloadDirectory = '';
String _fileDataUri = '';
bool _programmaticUrlChange = false;
String _lastUrlText = '';
@override
void initState() {
@@ -172,10 +174,15 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
}
void _handleUrlChanged() {
if (!_programmaticUrlChange && _fileDataUri.isNotEmpty) {
_fileDataUri = '';
final urlText = _urlController.text;
if (urlText != _lastUrlText) {
_lastUrlText = urlText;
_renameController.clear();
if (!_programmaticUrlChange && _fileDataUri.isNotEmpty) {
_fileDataUri = '';
}
}
_recognizeMagnetUri(_urlController.text.trim());
_recognizeMagnetUri(urlText.trim());
}
void _applyInitialTask(CreateTask? task) {
@@ -220,9 +227,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
}
}
if (opts != null) {
if (opts.name.isNotEmpty) {
_renameController.text = opts.name;
}
if (opts.path.isNotEmpty) {
_directoryController.text = opts.path;
}
@@ -370,18 +374,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
const SizedBox(height: 16),
_FormRow(
label: context.l10n.connections,
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: AppDesignTokens.settingsNumberControlWidth,
child: AppNumberInput(
fieldKey: const ValueKey('create-task-connections-input'),
controller: _connectionsController,
min: 1,
max: 256,
hintText: context.l10n.enterCount,
),
),
child: AppNumberInput(
fieldKey: const ValueKey('create-task-connections-input'),
controller: _connectionsController,
min: 1,
max: 256,
hintText: context.l10n.enterCount,
),
),
const SizedBox(height: 16),
@@ -396,10 +394,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
pickerKey: const ValueKey('create-task-directory-picker'),
controller: _directoryController,
hintText: context.l10n.chooseDownloadDirectory,
filled: true,
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
onChanged: (_) {
if (_asDefaultPath) {
setState(() => _asDefaultPath = false);
@@ -707,16 +701,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
child: SizedBox(width: 68, child: Center(child: Text(context.l10n.cancel))),
),
const SizedBox(width: 12),
AppPrimaryButton(
onPressed: _creating ? null : _confirm,
child: SizedBox(
width: 68,
child: Center(
child: _creating
? const SizedBox.square(dimension: 14, child: CircularProgressIndicator())
: Text(context.l10n.confirm),
),
),
AppLoadingButton(
key: const ValueKey('create-task-confirm-button'),
onPressed: _confirm,
loading: _creating,
variant: AppLoadingButtonVariant.primary,
child: SizedBox(width: 68, child: Center(child: Text(context.l10n.confirm))),
),
],
),
@@ -737,6 +727,19 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
await windowManager.close();
}
Future<void> _finishCreation() async {
if (widget.windowController != null) {
// Navigation is best-effort after a successful submission. A navigation
// failure must not leave a completed form available for resubmission.
try {
await ref.read(appCapabilitiesProvider).navigation.showDownloadingTasks();
} catch (error) {
debugPrint('Unable to select downloading tasks: $error');
}
}
await _closeWindow();
}
Future<void> _loadDefaults() async {
try {
final config = await ref.read(gopeedServiceProvider).getConfig();
@@ -863,7 +866,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
);
}),
);
await _closeWindow();
await _finishCreation();
return;
}
@@ -871,7 +874,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
final options = _buildOptions();
final result = await ref.read(gopeedServiceProvider).resolve(ResolveTask(req: request, opts: options));
if (!mounted) return;
_syncResolvedName(result);
final created = await _showResolveDialog(request, result);
if (!created) {
if (mounted) {
@@ -879,7 +881,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
}
return;
}
await _closeWindow();
await _finishCreation();
} catch (error) {
if (mounted) {
_showToast(error.toString());
@@ -1156,19 +1158,11 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
),
),
actions: [
SecondaryButton(
onPressed: submitting ? null : () => closeOverlay(dialogContext, false),
child: SizedBox(width: 68, child: Center(child: Text(dialogContext.l10n.cancel))),
),
AppPrimaryButton(
onPressed: submitting ? null : submit,
child: SizedBox(
width: 68,
child: Center(
child: submitting
? const SizedBox.square(dimension: 14, child: CircularProgressIndicator())
: Text(dialogContext.l10n.create),
),
flutter.Flexible(
child: ResolveTaskActions(
submitting: submitting,
onCancel: () => closeOverlay(dialogContext, false),
onCreate: submit,
),
),
],
@@ -1331,12 +1325,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
filterController.dispose();
}
void _syncResolvedName(ResolveResult result) {
if (_renameController.text.trim().isEmpty && result.res.name.trim().isNotEmpty) {
_renameController.text = result.res.name;
}
}
void _setUrlText(String value, {String fileDataUri = ''}) {
_programmaticUrlChange = true;
_urlController.text = value;
@@ -1678,17 +1666,12 @@ class _WindowTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return AppTextField(
controller: controller,
hintText: hintText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
obscureText: obscureText,
filled: true,
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
);
}
}
@@ -61,6 +61,8 @@ class _ResolveFileTreeState extends State<ResolveFileTree> {
child: FileTreeView<int>(
items: _treeItems,
keyPrefix: 'resolve-tree',
rowHeight: _resolveTreeRowHeight,
contentTextStyle: _resolveFileNameStyle,
headerLeading: ExcludeFocus(
child: Checkbox(
state: allSelected
@@ -336,3 +338,5 @@ extension on _FileTypeFilter {
const _videoExts = <String>{'mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v'};
const _audioExts = <String>{'mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'ape'};
const _imageExts = <String>{'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'};
const _resolveTreeRowHeight = 36.0;
const _resolveFileNameStyle = TextStyle(fontSize: 14, height: 1.25);
@@ -0,0 +1,50 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter/widgets.dart' as flutter;
import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/widgets/app_loading_button.dart';
class ResolveTaskActions extends StatelessWidget {
const ResolveTaskActions({super.key, required this.submitting, required this.onCancel, required this.onCreate});
final bool submitting;
final VoidCallback onCancel;
final VoidCallback onCreate;
@override
Widget build(BuildContext context) {
// Size both actions from the wider translated label, including while the
// create button replaces its label with a loader.
return IntrinsicWidth(
child: flutter.Row(
mainAxisSize: MainAxisSize.min,
children: [
flutter.Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: AppDesignTokens.dialogActionMinWidth),
child: SecondaryButton(
key: const ValueKey('resolve-cancel-button'),
onPressed: submitting ? null : onCancel,
child: Text(context.l10n.cancel, maxLines: 1, softWrap: false),
),
),
),
const SizedBox(width: AppDesignTokens.space8),
flutter.Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: AppDesignTokens.dialogActionMinWidth),
child: AppLoadingButton(
key: const ValueKey('resolve-create-button'),
onPressed: onCreate,
loading: submitting,
variant: AppLoadingButtonVariant.primary,
child: Text(context.l10n.createAction, maxLines: 1, softWrap: false),
),
),
),
],
),
);
}
}