From 38c2f1e8c218cba091e21fb480fff3e605b3d92e Mon Sep 17 00:00:00 2001 From: Levi Date: Sat, 14 Nov 2026 11:30:33 +0800 Subject: [PATCH] refactor(ui): refine advanced options and settings feedback (#1661) --- .../application/settings_controller.dart | 21 ++++++- .../presentation/pages/settings_page.dart | 7 ++- .../pages/create_task_window_page.dart | 44 ++------------ .../widgets/create_task_advanced_toggle.dart | 49 +++++++++++++++ ui/flutter/lib/l10n/app_ar.arb | 2 +- ui/flutter/lib/l10n/app_az.arb | 2 +- ui/flutter/lib/l10n/app_ca.arb | 2 +- ui/flutter/lib/l10n/app_de.arb | 2 +- ui/flutter/lib/l10n/app_en.arb | 2 +- ui/flutter/lib/l10n/app_es.arb | 2 +- ui/flutter/lib/l10n/app_fa.arb | 2 +- ui/flutter/lib/l10n/app_fr.arb | 2 +- ui/flutter/lib/l10n/app_hu.arb | 2 +- ui/flutter/lib/l10n/app_id.arb | 2 +- ui/flutter/lib/l10n/app_it.arb | 2 +- ui/flutter/lib/l10n/app_ja.arb | 2 +- ui/flutter/lib/l10n/app_ko.arb | 2 +- ui/flutter/lib/l10n/app_pl.arb | 2 +- ui/flutter/lib/l10n/app_pt.arb | 2 +- ui/flutter/lib/l10n/app_ru.arb | 2 +- ui/flutter/lib/l10n/app_sk.arb | 2 +- ui/flutter/lib/l10n/app_ta.arb | 2 +- ui/flutter/lib/l10n/app_tr.arb | 2 +- ui/flutter/lib/l10n/app_uk.arb | 2 +- ui/flutter/lib/l10n/app_vi.arb | 2 +- ui/flutter/lib/l10n/app_zh.arb | 2 +- ui/flutter/lib/l10n/app_zh_TW.arb | 2 +- .../application/settings_controller_test.dart | 40 ++++++++++++- .../create_task_advanced_toggle_test.dart | 59 +++++++++++++++++++ ui/flutter/test/widget_test.dart | 45 +++++++++++++- 30 files changed, 243 insertions(+), 68 deletions(-) create mode 100644 ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart create mode 100644 ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart diff --git a/ui/flutter/lib/features/settings/application/settings_controller.dart b/ui/flutter/lib/features/settings/application/settings_controller.dart index 311be7be..fdc33f19 100644 --- a/ui/flutter/lib/features/settings/application/settings_controller.dart +++ b/ui/flutter/lib/features/settings/application/settings_controller.dart @@ -19,6 +19,12 @@ class SettingsState { } } +class SettingsSaveResult { + const SettingsSaveResult({required this.btRestartRequired}); + + final bool btRestartRequired; +} + class SettingsController extends AsyncNotifier { Future _saveTail = Future.value(); int _saveRevision = 0; @@ -36,7 +42,7 @@ class SettingsController extends AsyncNotifier { state = AsyncValue.data(SettingsState(config: config)); } - Future save(DownloaderConfig config) { + Future 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 { return result; } - Future _save(DownloaderConfig config, int revision) async { + Future _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 { 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)); diff --git a/ui/flutter/lib/features/settings/presentation/pages/settings_page.dart b/ui/flutter/lib/features/settings/presentation/pages/settings_page.dart index c48a031e..0f90b6b7 100644 --- a/ui/flutter/lib/features/settings/presentation/pages/settings_page.dart +++ b/ui/flutter/lib/features/settings/presentation/pages/settings_page.dart @@ -625,7 +625,6 @@ class _SettingsPageState extends ConsumerState { ), 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 { ), 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 { 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()); diff --git a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart index abc96c13..5ffb71a2 100644 --- a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart +++ b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart @@ -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 { ), 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}); diff --git a/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart b/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart new file mode 100644 index 00000000..aa8e6112 --- /dev/null +++ b/ui/flutter/lib/features/tasks/presentation/widgets/create_task_advanced_toggle.dart @@ -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), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/ui/flutter/lib/l10n/app_ar.arb b/ui/flutter/lib/l10n/app_ar.arb index 6123f256..4b84f11e 100644 --- a/ui/flutter/lib/l10n/app_ar.arb +++ b/ui/flutter/lib/l10n/app_ar.arb @@ -208,7 +208,7 @@ "categoryName": "اسم الفئة", "categoryPath": "مسار الفئة", "autoCategorizeByExtension": "تصنيف تلقائي حسب نوع الملف", - "autoCategorizeByExtensionDescription": "توجيه التنزيلات إلى مجلد الفئة تلقائيًا حسب امتداد الملف عند الحفظ في مجلد التنزيل الافتراضي.", + "autoCategorizeByExtensionDescription": "توجيه التنزيلات إلى مجلد الفئة تلقائيًا حسب امتداد الملف عند الحفظ في مجلد التنزيل الافتراضي", "categoryExtensions": "امتدادات الملفات", "categoryExtensionsUsageHint": "افصل الامتدادات بفواصل أو مسافات ثم اضغط Enter.", "categoryExtensionsEmptyHint": "بدون الامتدادات، لن تُحفظ الملفات في هذا التصنيف تلقائيًا.", diff --git a/ui/flutter/lib/l10n/app_az.arb b/ui/flutter/lib/l10n/app_az.arb index 577b1937..5bbbec9b 100644 --- a/ui/flutter/lib/l10n/app_az.arb +++ b/ui/flutter/lib/l10n/app_az.arb @@ -215,7 +215,7 @@ "categoryName": "Kateqoriyanın Adı", "categoryPath": "Kateqoriyanın Yolu", "autoCategorizeByExtension": "Fayl tipinə görə avtomatik kateqoriyalaşdırma", - "autoCategorizeByExtensionDescription": "Standart yükləmə qovluğuna yadda saxlanarkən yükləmələr fayl uzantısına görə avtomatik olaraq kateqoriya qovluğuna yönləndirilir.", + "autoCategorizeByExtensionDescription": "Standart yükləmə qovluğuna yadda saxlanarkən yükləmələr fayl uzantısına görə avtomatik olaraq kateqoriya qovluğuna yönləndirilir", "categoryExtensions": "Fayl uzantıları", "categoryExtensionsUsageHint": "Uzantıları vergül və ya boşluqla ayırın, sonra Enter-a basın.", "categoryExtensionsEmptyHint": "Uzantı yoxdursa, fayllar bu kateqoriyaya avtomatik olaraq saxlanmır.", diff --git a/ui/flutter/lib/l10n/app_ca.arb b/ui/flutter/lib/l10n/app_ca.arb index 4e8fd62e..3cfb47fe 100644 --- a/ui/flutter/lib/l10n/app_ca.arb +++ b/ui/flutter/lib/l10n/app_ca.arb @@ -208,7 +208,7 @@ "categoryName": "Nom de la categoria", "categoryPath": "Ruta de la categoria", "autoCategorizeByExtension": "Categoritza automàticament segons el tipus de fitxer", - "autoCategorizeByExtensionDescription": "Les baixades es desen a la carpeta de categoria segons l'extensió del fitxer en utilitzar la carpeta de baixades predeterminada.", + "autoCategorizeByExtensionDescription": "Les baixades es desen a la carpeta de categoria segons l'extensió del fitxer en utilitzar la carpeta de baixades predeterminada", "categoryExtensions": "Extensions de fitxer", "categoryExtensionsUsageHint": "Separa les extensions amb comes o espais i prem Retorn.", "categoryExtensionsEmptyHint": "Sense extensions, els fitxers no es desaran aquí automàticament.", diff --git a/ui/flutter/lib/l10n/app_de.arb b/ui/flutter/lib/l10n/app_de.arb index 8e46d196..d05ccf6d 100644 --- a/ui/flutter/lib/l10n/app_de.arb +++ b/ui/flutter/lib/l10n/app_de.arb @@ -208,7 +208,7 @@ "categoryName": "Kategoriename", "categoryPath": "Kategoriepfad", "autoCategorizeByExtension": "Nach Dateityp automatisch kategorisieren", - "autoCategorizeByExtensionDescription": "Downloads werden beim Speichern im Standard-Download-Ordner anhand der Dateiendung automatisch einem Kategorieordner zugeordnet.", + "autoCategorizeByExtensionDescription": "Downloads werden beim Speichern im Standard-Download-Ordner anhand der Dateiendung automatisch einem Kategorieordner zugeordnet", "categoryExtensions": "Dateiendungen", "categoryExtensionsUsageHint": "Trenne Endungen mit Kommas oder Leerzeichen und drücke Enter.", "categoryExtensionsEmptyHint": "Ohne Endungen werden Dateien nicht automatisch dieser Kategorie zugeordnet.", diff --git a/ui/flutter/lib/l10n/app_en.arb b/ui/flutter/lib/l10n/app_en.arb index 3816a3fc..44ed0382 100644 --- a/ui/flutter/lib/l10n/app_en.arb +++ b/ui/flutter/lib/l10n/app_en.arb @@ -222,7 +222,7 @@ "categoryName": "Category Name", "categoryPath": "Category Path", "autoCategorizeByExtension": "Auto-categorize by file type", - "autoCategorizeByExtensionDescription": "Automatically route downloads into a category folder based on the file name when saving to the default download folder.", + "autoCategorizeByExtensionDescription": "Automatically route downloads into a category folder based on the file name when saving to the default download folder", "categoryExtensions": "Associated Extensions", "categoryExtensionsUsageHint": "Separate suffixes with commas or spaces, then press Enter.", "categoryExtensionsEmptyHint": "Without extensions, files won't be saved into this category automatically.", diff --git a/ui/flutter/lib/l10n/app_es.arb b/ui/flutter/lib/l10n/app_es.arb index d2daef8c..5a1110b7 100644 --- a/ui/flutter/lib/l10n/app_es.arb +++ b/ui/flutter/lib/l10n/app_es.arb @@ -208,7 +208,7 @@ "categoryName": "Nombre de categoría", "categoryPath": "Ruta de categoría", "autoCategorizeByExtension": "Categorizar automáticamente por tipo de archivo", - "autoCategorizeByExtensionDescription": "Las descargas se guardan automáticamente en la carpeta de categoría según la extensión del archivo al usar la carpeta de descargas predeterminada.", + "autoCategorizeByExtensionDescription": "Las descargas se guardan automáticamente en la carpeta de categoría según la extensión del archivo al usar la carpeta de descargas predeterminada", "categoryExtensions": "Extensiones de archivo", "categoryExtensionsUsageHint": "Separa las extensiones con comas o espacios y pulsa Intro.", "categoryExtensionsEmptyHint": "Sin extensiones, los archivos no se guardarán automáticamente en esta categoría.", diff --git a/ui/flutter/lib/l10n/app_fa.arb b/ui/flutter/lib/l10n/app_fa.arb index 97478f13..3459113a 100644 --- a/ui/flutter/lib/l10n/app_fa.arb +++ b/ui/flutter/lib/l10n/app_fa.arb @@ -208,7 +208,7 @@ "categoryName": "نام دسته‌بندی", "categoryPath": "مسیر دسته‌بندی", "autoCategorizeByExtension": "دسته‌بندی خودکار بر اساس نوع فایل", - "autoCategorizeByExtensionDescription": "هنگام ذخیره در پوشه پیش‌فرض دانلود، دانلودها بر اساس پسوند فایل به پوشه دسته مربوطه منتقل می‌شوند.", + "autoCategorizeByExtensionDescription": "هنگام ذخیره در پوشه پیش‌فرض دانلود، دانلودها بر اساس پسوند فایل به پوشه دسته مربوطه منتقل می‌شوند", "categoryExtensions": "پسوندهای فایل", "categoryExtensionsUsageHint": "پسوندها را با ویرگول یا فاصله جدا کنید و Enter را بزنید.", "categoryExtensionsEmptyHint": "بدون پسوندها، فایل‌ها به‌صورت خودکار در این دسته ذخیره نمی‌شوند.", diff --git a/ui/flutter/lib/l10n/app_fr.arb b/ui/flutter/lib/l10n/app_fr.arb index 23566819..e3978284 100644 --- a/ui/flutter/lib/l10n/app_fr.arb +++ b/ui/flutter/lib/l10n/app_fr.arb @@ -208,7 +208,7 @@ "categoryName": "Nom de la catégorie", "categoryPath": "Chemin de la catégorie", "autoCategorizeByExtension": "Catégoriser automatiquement selon le type de fichier", - "autoCategorizeByExtensionDescription": "Les téléchargements sont placés dans un dossier de catégorie selon l'extension du fichier lors de l'enregistrement dans le dossier de téléchargement par défaut.", + "autoCategorizeByExtensionDescription": "Les téléchargements sont placés dans un dossier de catégorie selon l'extension du fichier lors de l'enregistrement dans le dossier de téléchargement par défaut", "categoryExtensions": "Extensions de fichier", "categoryExtensionsUsageHint": "Séparez les extensions par virgules ou espaces, puis appuyez sur Entrée.", "categoryExtensionsEmptyHint": "Sans extensions, les fichiers ne seront pas enregistrés ici automatiquement.", diff --git a/ui/flutter/lib/l10n/app_hu.arb b/ui/flutter/lib/l10n/app_hu.arb index d2e3cbc2..2df11da3 100644 --- a/ui/flutter/lib/l10n/app_hu.arb +++ b/ui/flutter/lib/l10n/app_hu.arb @@ -208,7 +208,7 @@ "categoryName": "Kategória neve", "categoryPath": "Kategória útvonala", "autoCategorizeByExtension": "Automatikus kategorizálás fájltípus szerint", - "autoCategorizeByExtensionDescription": "Az alapértelmezett letöltési mappa használatakor a letöltések a fájl kiterjesztése alapján automatikusan a kategória mappájába kerülnek.", + "autoCategorizeByExtensionDescription": "Az alapértelmezett letöltési mappa használatakor a letöltések a fájl kiterjesztése alapján automatikusan a kategória mappájába kerülnek", "categoryExtensions": "Fájlkiterjesztések", "categoryExtensionsUsageHint": "A kiterjesztéseket vesszővel vagy szóközzel válaszd el, majd nyomd le az Enter billentyűt.", "categoryExtensionsEmptyHint": "Kiterjesztések nélkül a fájlok nem kerülnek automatikusan ebbe a kategóriába.", diff --git a/ui/flutter/lib/l10n/app_id.arb b/ui/flutter/lib/l10n/app_id.arb index a2383e10..b264ee12 100644 --- a/ui/flutter/lib/l10n/app_id.arb +++ b/ui/flutter/lib/l10n/app_id.arb @@ -208,7 +208,7 @@ "categoryName": "Nama Kategori", "categoryPath": "Path Kategori", "autoCategorizeByExtension": "Kategorikan otomatis berdasarkan jenis file", - "autoCategorizeByExtensionDescription": "Unduhan dialihkan ke folder kategori berdasarkan ekstensi file saat disimpan ke folder unduhan bawaan.", + "autoCategorizeByExtensionDescription": "Unduhan dialihkan ke folder kategori berdasarkan ekstensi file saat disimpan ke folder unduhan bawaan", "categoryExtensions": "Ekstensi file", "categoryExtensionsUsageHint": "Pisahkan ekstensi dengan koma atau spasi, lalu tekan Enter.", "categoryExtensionsEmptyHint": "Tanpa ekstensi, file tidak akan otomatis disimpan ke kategori ini.", diff --git a/ui/flutter/lib/l10n/app_it.arb b/ui/flutter/lib/l10n/app_it.arb index 1a8df506..8f4afd36 100644 --- a/ui/flutter/lib/l10n/app_it.arb +++ b/ui/flutter/lib/l10n/app_it.arb @@ -208,7 +208,7 @@ "categoryName": "Nome categoria", "categoryPath": "Percorso categoria", "autoCategorizeByExtension": "Categorizza automaticamente per tipo di file", - "autoCategorizeByExtensionDescription": "Il download viene instradato automaticamente nella cartella della categoria in base all'estensione quando si salva nella cartella di download predefinita.", + "autoCategorizeByExtensionDescription": "Il download viene instradato automaticamente nella cartella della categoria in base all'estensione quando si salva nella cartella di download predefinita", "categoryExtensions": "Estensioni file", "categoryExtensionsUsageHint": "Separa le estensioni con virgole o spazi e premi Invio.", "categoryExtensionsEmptyHint": "Senza estensioni, i file non verranno salvati qui automaticamente.", diff --git a/ui/flutter/lib/l10n/app_ja.arb b/ui/flutter/lib/l10n/app_ja.arb index 2ffb0d45..68f60558 100644 --- a/ui/flutter/lib/l10n/app_ja.arb +++ b/ui/flutter/lib/l10n/app_ja.arb @@ -208,7 +208,7 @@ "categoryName": "カテゴリ名", "categoryPath": "カテゴリパス", "autoCategorizeByExtension": "ファイル種別で自動分類", - "autoCategorizeByExtensionDescription": "既定のダウンロードフォルダーに保存する際、拡張子に基づいて分類フォルダーへ自動的に振り分けます。", + "autoCategorizeByExtensionDescription": "既定のダウンロードフォルダーに保存する際、拡張子に基づいて分類フォルダーへ自動的に振り分けます", "categoryExtensions": "ファイル拡張子", "categoryExtensionsUsageHint": "カンマやスペースで拡張子を区切り、Enterで追加します。", "categoryExtensionsEmptyHint": "拡張子が未設定の場合、ファイルは自動的にこの分類へ保存されません。", diff --git a/ui/flutter/lib/l10n/app_ko.arb b/ui/flutter/lib/l10n/app_ko.arb index 1973cbd0..181a31a1 100644 --- a/ui/flutter/lib/l10n/app_ko.arb +++ b/ui/flutter/lib/l10n/app_ko.arb @@ -208,7 +208,7 @@ "categoryName": "카테고리 이름", "categoryPath": "카테고리 경로", "autoCategorizeByExtension": "파일 형식별 자동 분류", - "autoCategorizeByExtensionDescription": "기본 다운로드 폴더에 저장할 때 확장자에 따라 분류 폴더로 자동 이동합니다.", + "autoCategorizeByExtensionDescription": "기본 다운로드 폴더에 저장할 때 확장자에 따라 분류 폴더로 자동 이동합니다", "categoryExtensions": "파일 확장자", "categoryExtensionsUsageHint": "쉼표나 공백으로 확장자를 구분하고 Enter를 눌러 추가하세요.", "categoryExtensionsEmptyHint": "확장자가 없으면 파일이 이 분류로 자동 저장되지 않습니다.", diff --git a/ui/flutter/lib/l10n/app_pl.arb b/ui/flutter/lib/l10n/app_pl.arb index d953dbaa..9d467855 100644 --- a/ui/flutter/lib/l10n/app_pl.arb +++ b/ui/flutter/lib/l10n/app_pl.arb @@ -208,7 +208,7 @@ "categoryName": "Nazwa kategorii", "categoryPath": "Ścieżka kategorii", "autoCategorizeByExtension": "Automatyczna kategorizacja według typu pliku", - "autoCategorizeByExtensionDescription": "Pobierane pliki są automatycznie zapisywane w folderze kategorii na podstawie rozszerzenia, gdy używany jest domyślny folder pobierania.", + "autoCategorizeByExtensionDescription": "Pobierane pliki są automatycznie zapisywane w folderze kategorii na podstawie rozszerzenia, gdy używany jest domyślny folder pobierania", "categoryExtensions": "Rozszerzenia plików", "categoryExtensionsUsageHint": "Rozdzielaj rozszerzenia przecinkami lub spacjami i naciskaj Enter.", "categoryExtensionsEmptyHint": "Bez rozszerzeń pliki nie będą automatycznie zapisywane w tej kategorii.", diff --git a/ui/flutter/lib/l10n/app_pt.arb b/ui/flutter/lib/l10n/app_pt.arb index 464e7960..2ee1a4f7 100644 --- a/ui/flutter/lib/l10n/app_pt.arb +++ b/ui/flutter/lib/l10n/app_pt.arb @@ -208,7 +208,7 @@ "categoryName": "Nome da Categoria", "categoryPath": "Caminho da Categoria", "autoCategorizeByExtension": "Categorizar automaticamente por tipo de arquivo", - "autoCategorizeByExtensionDescription": "Envia os downloads para a pasta da categoria com base na extensão ao salvar na pasta de download padrão.", + "autoCategorizeByExtensionDescription": "Envia os downloads para a pasta da categoria com base na extensão ao salvar na pasta de download padrão", "categoryExtensions": "Extensões de arquivo", "categoryExtensionsUsageHint": "Separe as extensões com vírgulas ou espaços e prima Enter.", "categoryExtensionsEmptyHint": "Sem extensões, os ficheiros não serão guardados aqui automaticamente.", diff --git a/ui/flutter/lib/l10n/app_ru.arb b/ui/flutter/lib/l10n/app_ru.arb index fd9abe4a..4e6bd7cc 100644 --- a/ui/flutter/lib/l10n/app_ru.arb +++ b/ui/flutter/lib/l10n/app_ru.arb @@ -208,7 +208,7 @@ "categoryName": "Название категории", "categoryPath": "Путь категории", "autoCategorizeByExtension": "Автокатегоризация по типу файла", - "autoCategorizeByExtensionDescription": "Загрузки автоматически сохраняются в папку категории по расширению файла, если выбрана папка загрузки по умолчанию.", + "autoCategorizeByExtensionDescription": "Загрузки автоматически сохраняются в папку категории по расширению файла, если выбрана папка загрузки по умолчанию", "categoryExtensions": "Расширения файлов", "categoryExtensionsUsageHint": "Разделяйте расширения запятыми или пробелами и нажимайте Enter.", "categoryExtensionsEmptyHint": "Без расширений файлы не будут автоматически сохранены в эту категорию.", diff --git a/ui/flutter/lib/l10n/app_sk.arb b/ui/flutter/lib/l10n/app_sk.arb index afdf6695..4e9d3c16 100644 --- a/ui/flutter/lib/l10n/app_sk.arb +++ b/ui/flutter/lib/l10n/app_sk.arb @@ -222,7 +222,7 @@ "categoryName": "Názov kategórie", "categoryPath": "Cesta kategórie", "autoCategorizeByExtension": "Automatická kategorizácia podľa typu súboru", - "autoCategorizeByExtensionDescription": "Sťahovania sa pri ukladaní do predvoleného priečinka sťahovania automaticky presúvajú do priečinka kategórie podľa prípony súboru.", + "autoCategorizeByExtensionDescription": "Sťahovania sa pri ukladaní do predvoleného priečinka sťahovania automaticky presúvajú do priečinka kategórie podľa prípony súboru", "categoryExtensions": "Prípony súborov", "categoryExtensionsUsageHint": "Oddeľujte prípony čiarkami alebo medzerami a stlačte Enter.", "categoryExtensionsEmptyHint": "Bez prípon sa súbory automaticky neuložia do tejto kategórie.", diff --git a/ui/flutter/lib/l10n/app_ta.arb b/ui/flutter/lib/l10n/app_ta.arb index 44acf778..d2adc8b3 100644 --- a/ui/flutter/lib/l10n/app_ta.arb +++ b/ui/flutter/lib/l10n/app_ta.arb @@ -208,7 +208,7 @@ "categoryName": "வகை பெயர்", "categoryPath": "வகை பாதை", "autoCategorizeByExtension": "கோப்பு வகை வாரியாக தானாக வகைப்படுத்து", - "autoCategorizeByExtensionDescription": "இயல்புநிலை பதிவிறக்கக் கோப்புறையில் சேமிக்கும்போது, கோப்பு நீட்டிப்பின் அடிப்படையில் வகைக் கோப்புறைக்கு தானாக அனுப்பப்படும்.", + "autoCategorizeByExtensionDescription": "இயல்புநிலை பதிவிறக்கக் கோப்புறையில் சேமிக்கும்போது, கோப்பு நீட்டிப்பின் அடிப்படையில் வகைக் கோப்புறைக்கு தானாக அனுப்பப்படும்", "categoryExtensions": "கோப்பு நீட்டிப்புகள்", "categoryExtensionsUsageHint": "நீட்டிப்புகளை காற்புள்ளிகள் அல்லது இடைவெளிகளால் பிரித்து Enter அழுத்தவும்.", "categoryExtensionsEmptyHint": "நீட்டிப்புகள் இல்லாததால், கோப்புகள் இந்த வகைப்பாட்டில் தானாகச் சேமிக்கப்படாது.", diff --git a/ui/flutter/lib/l10n/app_tr.arb b/ui/flutter/lib/l10n/app_tr.arb index 7fe573c7..3d24bba7 100644 --- a/ui/flutter/lib/l10n/app_tr.arb +++ b/ui/flutter/lib/l10n/app_tr.arb @@ -208,7 +208,7 @@ "categoryName": "Kategori Adı", "categoryPath": "Kategori Yolu", "autoCategorizeByExtension": "Dosya tipine göre otomatik sınıflandır", - "autoCategorizeByExtensionDescription": "Varsayılan indirme klasörüne kaydedilirken dosya uzantısına göre ilgili klasöre otomatik yönlendirir.", + "autoCategorizeByExtensionDescription": "Varsayılan indirme klasörüne kaydedilirken dosya uzantısına göre ilgili klasöre otomatik yönlendirir", "categoryExtensions": "Dosya uzantıları", "categoryExtensionsUsageHint": "Sonekleri virgül veya boşlukla ayırıp Enter'a basın.", "categoryExtensionsEmptyHint": "Uzantı yoksa dosyalar bu kategoriye otomatik olarak kaydedilmez.", diff --git a/ui/flutter/lib/l10n/app_uk.arb b/ui/flutter/lib/l10n/app_uk.arb index 8dc51080..e41f7f83 100644 --- a/ui/flutter/lib/l10n/app_uk.arb +++ b/ui/flutter/lib/l10n/app_uk.arb @@ -208,7 +208,7 @@ "categoryName": "Назва категорії", "categoryPath": "Шлях категорії", "autoCategorizeByExtension": "Автокатегоризація за типом файлу", - "autoCategorizeByExtensionDescription": "Завантаження автоматично зберігаються в папку категорії за розширенням файлу, якщо вибрано папку завантаження за замовчуванням.", + "autoCategorizeByExtensionDescription": "Завантаження автоматично зберігаються в папку категорії за розширенням файлу, якщо вибрано папку завантаження за замовчуванням", "categoryExtensions": "Розширення файлів", "categoryExtensionsUsageHint": "Розділяйте розширення комами або пробілами та натискайте Enter.", "categoryExtensionsEmptyHint": "Без розширень файли не зберігатимуться в цю категорію автоматично.", diff --git a/ui/flutter/lib/l10n/app_vi.arb b/ui/flutter/lib/l10n/app_vi.arb index 91d7cf89..5baa7d08 100644 --- a/ui/flutter/lib/l10n/app_vi.arb +++ b/ui/flutter/lib/l10n/app_vi.arb @@ -208,7 +208,7 @@ "categoryName": "Tên danh mục", "categoryPath": "Đường dẫn danh mục", "autoCategorizeByExtension": "Tự động phân loại theo loại tệp", - "autoCategorizeByExtensionDescription": "Tải xuống sẽ được chuyển vào thư mục danh mục dựa trên phần mở rộng tệp khi lưu vào thư mục tải xuống mặc định.", + "autoCategorizeByExtensionDescription": "Tải xuống sẽ được chuyển vào thư mục danh mục dựa trên phần mở rộng tệp khi lưu vào thư mục tải xuống mặc định", "categoryExtensions": "Phần mở rộng tệp", "categoryExtensionsUsageHint": "Phân tách phần mở rộng bằng dấu phẩy hoặc khoảng trắng rồi nhấn Enter.", "categoryExtensionsEmptyHint": "Không có phần mở rộng, tệp sẽ không tự động được lưu vào danh mục này.", diff --git a/ui/flutter/lib/l10n/app_zh.arb b/ui/flutter/lib/l10n/app_zh.arb index 56eab1d9..9b0ea544 100644 --- a/ui/flutter/lib/l10n/app_zh.arb +++ b/ui/flutter/lib/l10n/app_zh.arb @@ -208,7 +208,7 @@ "categoryName": "分类名称", "categoryPath": "分类路径", "autoCategorizeByExtension": "按文件类型自动分类", - "autoCategorizeByExtensionDescription": "保存到默认下载目录时,根据文件后缀自动归入对应分类文件夹。", + "autoCategorizeByExtensionDescription": "保存到默认下载目录时,根据文件后缀自动归入对应分类文件夹", "categoryExtensions": "文件后缀关联", "categoryExtensionsUsageHint": "支持逗号/空格分隔后缀,按回车添加。", "categoryExtensionsEmptyHint": "未设置后缀,文件不会自动归入此分类。", diff --git a/ui/flutter/lib/l10n/app_zh_TW.arb b/ui/flutter/lib/l10n/app_zh_TW.arb index 0b36e96c..8eb05b56 100644 --- a/ui/flutter/lib/l10n/app_zh_TW.arb +++ b/ui/flutter/lib/l10n/app_zh_TW.arb @@ -208,7 +208,7 @@ "categoryName": "分類名稱", "categoryPath": "分類路徑", "autoCategorizeByExtension": "按檔案類型自動分類", - "autoCategorizeByExtensionDescription": "儲存至預設下載目錄時,根據檔案副檔名自動歸入對應分類資料夾。", + "autoCategorizeByExtensionDescription": "儲存至預設下載目錄時,根據檔案副檔名自動歸入對應分類資料夾", "categoryExtensions": "檔案副檔名", "categoryExtensionsUsageHint": "支援逗號/空白分隔後綴,按回車新增。", "categoryExtensionsEmptyHint": "未設定後綴,檔案不會自動歸入此分類。", diff --git a/ui/flutter/test/features/settings/application/settings_controller_test.dart b/ui/flutter/test/features/settings/application/settings_controller_test.dart index 0d6816f7..133160dc 100644 --- a/ui/flutter/test/features/settings/application/settings_controller_test.dart +++ b/ui/flutter/test/features/settings/application/settings_controller_test.dart @@ -99,6 +99,43 @@ void main() { expect(saved.saving, isFalse); }); + for (final field in ['port', 'DHT', 'PEX']) { + test('$field restart notice follows successful persisted changes only', () async { + var backend = DownloaderConfig(); + var failNextSave = false; + final registry = CapabilityRegistry(createAppCapabilityCodecs()) + ..bind(GopeedMethods.getConfig, (_) => DownloaderConfig.fromJson(backend.toJson())) + ..bind(GopeedMethods.putConfig, (config) { + if (failNextSave) { + failNextSave = false; + throw StateError('offline'); + } + backend = DownloaderConfig.fromJson(config.toJson()); + return const RpcUnit(); + }); + final container = _settingsContainer(registry); + addTearDown(container.dispose); + final initial = await container.read(settingsControllerProvider.future); + final controller = container.read(settingsControllerProvider.notifier); + final draft = DownloaderConfig.fromJson(initial.config.toJson()); + switch (field) { + case 'port': + draft.protocolConfig.bt.listenPort = 6881; + case 'DHT': + draft.protocolConfig.bt.disableDht = true; + case 'PEX': + draft.protocolConfig.bt.disablePex = true; + } + failNextSave = true; + await expectLater(controller.save(draft), throwsStateError); + expect((await controller.save(draft)).btRestartRequired, isTrue); + expect((await controller.save(draft)).btRestartRequired, isFalse); + draft.protocolConfig.bt.seedRatio = 2; + expect((await controller.save(draft)).btRestartRequired, isFalse); + expect((await controller.save(initial.config)).btRestartRequired, isTrue); + }); + } + test('rapid full config saves run in order and keep the latest user edits', () async { final backend = DownloaderConfig(); final entered = Completer(); @@ -127,7 +164,8 @@ void main() { final savingSecond = controller.save(second); expect(puts.length, 1); release.complete(); - await Future.wait([savingFirst, savingSecond]); + final results = await Future.wait([savingFirst, savingSecond]); + expect(results.map((result) => result.btRestartRequired), [true, false]); expect(puts, hasLength(2)); expect(puts[0].protocolConfig.bt.listenPort, 6881); expect(puts[1].protocolConfig.bt.listenPort, 6881); diff --git a/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart b/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart new file mode 100644 index 00000000..34a5a5ee --- /dev/null +++ b/ui/flutter/test/features/tasks/presentation/create_task_advanced_toggle_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gopeed/features/tasks/presentation/widgets/create_task_advanced_toggle.dart'; +import 'package:gopeed/shared/theme/app_component_themes.dart'; +import 'package:gopeed/shared/theme/app_theme.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' as shad; + +void main() { + for (final width in [360.0, 768.0, 1024.0]) { + testWidgets('advanced disclosure supports keyboard and touch at width $width', (tester) async { + tester.view.physicalSize = Size(width, 768); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + var expanded = false; + await tester.pumpWidget( + shad.ShadcnApp( + theme: AppTheme.light(), + materialTheme: AppTheme.materialLight(), + home: AppComponentThemes( + child: StatefulBuilder( + builder: (context, setState) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: CreateTaskAdvancedToggle( + expanded: expanded, + onPressed: () => setState(() => expanded = !expanded), + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(tester.getSize(find.byType(shad.GhostButton)).height, greaterThanOrEqualTo(44)); + expect( + find.byWidgetPredicate((widget) => widget is Semantics && widget.properties.expanded == false), + findsOneWidget, + ); + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + expect(expanded, isTrue); + expect( + find.byWidgetPredicate((widget) => widget is Semantics && widget.properties.expanded == true), + findsOneWidget, + ); + await tester.tap(find.text('Advanced')); + await tester.pumpAndSettle(); + expect(expanded, isFalse); + expect(tester.takeException(), isNull); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); + }); + } +} diff --git a/ui/flutter/test/widget_test.dart b/ui/flutter/test/widget_test.dart index 34bc10d9..882e391c 100644 --- a/ui/flutter/test/widget_test.dart +++ b/ui/flutter/test/widget_test.dart @@ -1636,6 +1636,46 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('BT settings show a restart message only after an affected auto-save', (tester) async { + await _setTestSize(tester, const Size(1024, 900)); + final controller = FakeSettingsController()..btRestartRequired = true; + await tester.pumpWidget( + ProviderScope( + overrides: [ + appRuntimeControllerProvider.overrideWith(FakeRuntimeController.new), + appPlatformControllerProvider.overrideWith(FakePlatformController.new), + settingsControllerProvider.overrideWith(() => controller), + ], + child: const _SettingsTestApp(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Downloads')); + await tester.pumpAndSettle(); + const message = 'This setting takes effect after restarting the app'; + expect(find.text(message), findsNothing); + final port = find.byKey(const ValueKey('bt-listen-port-input')); + await tester.ensureVisible(port); + await tester.pumpAndSettle(); + await tester.enterText(port, '6881'); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.text(message), findsNothing); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 300)); + expect(find.text(message), findsOneWidget); + expect(tester.widget(find.byType(AppToastContent)).type, AppToastType.info); + await tester.pump(const Duration(seconds: 6)); + await tester.pumpAndSettle(); + controller.btRestartRequired = false; + final seedRatio = find.byKey(const ValueKey('bt-seed-ratio-input')); + await tester.ensureVisible(seedRatio); + await tester.enterText(seedRatio, '2'); + await tester.pump(const Duration(milliseconds: 700)); + await tester.pumpAndSettle(); + expect(find.text(message), findsNothing); + expect(tester.takeException(), isNull); + }); + testWidgets('settings FTP connections save into the FTP protocol config', (WidgetTester tester) async { await _setTestSize(tester, const Size(1024, 900)); final settingsController = FakeSettingsController(); @@ -6181,12 +6221,15 @@ ApiServerState _testApiServerState({ } class FakeSettingsController extends SettingsController { + bool btRestartRequired = false; + @override Future build() async => SettingsState(config: DownloaderConfig()); @override - Future save(DownloaderConfig config) async { + Future save(DownloaderConfig config) async { state = AsyncValue.data(SettingsState(config: config)); + return SettingsSaveResult(btRestartRequired: btRestartRequired); } }