Merge PR #1496 latest head: reuse shared file path picker

This commit is contained in:
Levi committed 2026-09-10 00:25:56 +08:00
commit 99a62357ab
4 files changed
+81 -15

No files matched your search

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Divider, Icons, Scrollbar, ScrollbarOrientation;
import 'package:flutter/widgets.dart';
@@ -1376,12 +1375,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
title: index == null ? context.l10n.addScript : context.l10n.editScript,
fieldLabel: context.l10n.scriptPath,
initialValue: index == null ? '' : paths[index],
pickPath: Util.isDesktop()
? () async {
final file = await FilePicker.pickFile();
return file?.path;
}
: null,
pickFile: true,
);
if (value == null || !mounted) return;
_mutateConfig((config) {
@@ -122,7 +122,7 @@ Future<String?> showTextSettingDialog(
required String fieldLabel,
String initialValue = '',
bool requireHttpUrl = false,
Future<String?> Function()? pickPath,
bool pickFile = false,
Future<void> Function(String value)? onTest,
}) async {
final controller = TextEditingController(text: initialValue);
@@ -189,16 +189,12 @@ Future<String?> showTextSettingDialog(
children: [
Text(fieldLabel, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
if (pickPath == null)
if (!pickFile)
AppTextField(controller: controller)
else
AppPathPickerField(
AppPathPickerField.file(
controller: controller,
desktopWidth: AppDesignTokens.settingsFormControlWidth,
onPick: () async {
final path = await pickPath();
if (path != null && path.isNotEmpty) controller.text = path;
},
),
if (validationMessage != null || testMessage != null) ...[
const SizedBox(height: 8),
@@ -1,8 +1,10 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../../core/utils/breakpoints.dart';
import '../../util/util.dart';
import '../services/download_directory_picker.dart';
import 'app_text_field.dart';
import 'app_tooltip.dart';
@@ -24,7 +26,28 @@ class AppPathPickerField extends StatefulWidget {
this.padding,
this.onChanged,
this.pickerButtonStyle = AppPathPickerButtonStyle.ghost,
}) : platformDownloadDirectory = false,
}) : platformFile = false,
platformDownloadDirectory = false,
onDirectoryPicked = null,
allowAndroidEditing = false;
/// Selects a native desktop file path; Web and mobile accept manual paths only.
const AppPathPickerField.file({
super.key,
required this.controller,
this.fieldKey,
this.pickerKey,
this.hintText,
this.desktopWidth,
this.filled,
this.border,
this.borderRadius,
this.padding,
this.onChanged,
this.pickerButtonStyle = AppPathPickerButtonStyle.outline,
}) : onPick = null,
platformFile = true,
platformDownloadDirectory = false,
onDirectoryPicked = null,
allowAndroidEditing = false;
@@ -44,6 +67,7 @@ class AppPathPickerField extends StatefulWidget {
this.onChanged,
this.pickerButtonStyle = AppPathPickerButtonStyle.outline,
}) : onPick = null,
platformFile = false,
platformDownloadDirectory = true;
final TextEditingController controller;
@@ -58,6 +82,7 @@ class AppPathPickerField extends StatefulWidget {
final EdgeInsetsGeometry? padding;
final ValueChanged<String>? onChanged;
final AppPathPickerButtonStyle pickerButtonStyle;
final bool platformFile;
final bool platformDownloadDirectory;
final ValueChanged<String>? onDirectoryPicked;
final bool allowAndroidEditing;
@@ -97,6 +122,16 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
setState(() => _tooltipPath = path);
}
Future<void> _pickFile() async {
if (!Util.isDesktop()) return;
final file = await FilePicker.pickFile();
if (!mounted) return;
final path = file?.path;
if (path == null || path.isEmpty || path == widget.controller.text) return;
widget.controller.text = path;
widget.onChanged?.call(path);
}
Future<void> _pickPlatformDirectory() async {
final path = await DownloadDirectoryPicker.pick(context, currentPath: widget.controller.text.trim());
if (!mounted || path == null || path.isEmpty || path == widget.controller.text) return;
@@ -110,6 +145,8 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
final desktop = MediaQuery.sizeOf(context).width >= Breakpoints.mobile;
final onPick = widget.platformDownloadDirectory
? (DownloadDirectoryPicker.canPick ? _pickPlatformDirectory : null)
: widget.platformFile
? (Util.isDesktop() ? _pickFile : null)
: widget.onPick;
return SizedBox(
width: desktop ? widget.desktopWidth : double.infinity,
@@ -0,0 +1,39 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/shared/theme/app_component_themes.dart';
import 'package:gopeed/shared/theme/app_theme.dart';
import 'package:gopeed/shared/widgets/app_path_picker_field.dart';
import 'package:gopeed/shared/widgets/app_text_field.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
void main() {
testWidgets('file paths remain editable and Web hides native file selection', (tester) async {
final controller = TextEditingController(text: '/scripts/old.sh');
addTearDown(controller.dispose);
await tester.pumpWidget(
shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: AppComponentThemes(
child: Center(
child: AppPathPickerField.file(
controller: controller,
desktopWidth: 320,
pickerKey: const ValueKey('file-picker'),
),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('file-picker')), kIsWeb ? findsNothing : findsOneWidget);
expect(tester.widget<AppTextField>(find.byType(AppTextField)).readOnly, isFalse);
await tester.enterText(find.byType(EditableText), '/scripts/custom.sh');
await tester.pump();
expect(controller.text, '/scripts/custom.sh');
expect(tester.takeException(), isNull);
});
}