From ec2b87933a520898ead508cdc52c0b1e055b41e6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 09:41:05 +0200 Subject: [PATCH] Feature Request: Support for setting download location per-device in Windows App #563 --- lib/redux/app/app_actions.dart | 2 + lib/redux/ui/pref_reducer.dart | 7 +++ lib/ui/settings/device_settings.dart | 76 +++++++++++++++++++++++++ lib/ui/settings/device_settings_vm.dart | 5 ++ lib/ui/settings/settings_list.dart | 1 + lib/utils/files.dart | 2 +- lib/utils/i18n.dart | 29 ++++++---- pubspec.foss.yaml | 1 + pubspec.lock | 8 +++ pubspec.yaml | 1 + 10 files changed, 121 insertions(+), 11 deletions(-) diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 028eb2229..b0ba340ab 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -188,6 +188,7 @@ class UpdateUserPreferences implements PersistPrefs { this.enableTooltips, this.flexibleSearch, this.enableNativeBrowser, + this.downloadsFolder, this.statementIncludes, }); @@ -219,6 +220,7 @@ class UpdateUserPreferences implements PersistPrefs { final bool? enableTooltips; final bool? flexibleSearch; final bool? enableNativeBrowser; + final String? downloadsFolder; final BuiltList? statementIncludes; } diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index 35d419d58..b2c43077c 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -93,6 +93,7 @@ PrefState prefReducer( longPressReducer(state.longPressSelectionIsDefault, action) ..tapSelectedToEdit = tapSelectedToEditReducer(state.tapSelectedToEdit, action) + ..donwloadsFolder = downloadsFolderReducer(state.donwloadsFolder, action) ..requireAuthentication = requireAuthenticationReducer(state.requireAuthentication, action) ..colorTheme = colorThemeReducer(state.colorTheme, action) @@ -412,6 +413,12 @@ Reducer tapSelectedToEditReducer = combineReducers([ }), ]); +Reducer downloadsFolderReducer = combineReducers([ + TypedReducer((downloadsFolder, action) { + return action.downloadsFolder ?? downloadsFolder; + }), +]); + Reducer isPreviewVisibleReducer = combineReducers([ TypedReducer((value, action) { return !value; diff --git a/lib/ui/settings/device_settings.dart b/lib/ui/settings/device_settings.dart index f25122f13..0707724cd 100644 --- a/lib/ui/settings/device_settings.dart +++ b/lib/ui/settings/device_settings.dart @@ -1,4 +1,7 @@ // Flutter imports: +import 'dart:io'; + +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' hide LiveText; @@ -7,6 +10,8 @@ import 'package:flutter/services.dart' hide LiveText; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -50,6 +55,11 @@ class _DeviceSettingsState extends State TabController? _controller; FocusScopeNode? _focusNode; + String _defaultDownloadsFolder = ''; + + final _downloadsFolderController = TextEditingController(); + + List _controllers = []; @override void initState() { @@ -61,6 +71,37 @@ class _DeviceSettingsState extends State _controller!.addListener(_onTabChanged); } + @override + void didChangeDependencies() async { + super.didChangeDependencies(); + + _controllers = [ + _downloadsFolderController, + ]; + + _controllers + .forEach((dynamic controller) => controller.removeListener(_onChanged)); + + final prefState = widget.viewModel.state.prefState; + _downloadsFolderController.text = prefState.donwloadsFolder; + + _controllers + .forEach((dynamic controller) => controller.addListener(_onChanged)); + + _defaultDownloadsFolder = prefState.donwloadsFolder.isEmpty + ? await getAppDownloadDirectory() ?? '' + : prefState.donwloadsFolder; + } + + void _onChanged() async { + widget.viewModel + .onDownloadsFolderChanged(context, _downloadsFolderController.text); + + _defaultDownloadsFolder = _downloadsFolderController.text.isEmpty + ? await getAppDownloadDirectory() ?? '' + : _downloadsFolderController.text; + } + void _onTabChanged() { final store = StoreProvider.of(context); store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index)); @@ -226,6 +267,41 @@ class _DeviceSettingsState extends State ), FormCard( children: [ + if (!kIsWeb) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DecoratedFormField( + label: localization.downloadsFolder, + keyboardType: TextInputType.text, + hint: _defaultDownloadsFolder, + controller: _downloadsFolderController, + ), + ), + SizedBox(width: 20), + OutlinedButton( + onPressed: () async { + final folder = await FilesystemPicker.open( + context: context, + fsType: FilesystemType.folder, + rootDirectory: Directory(Platform.pathSeparator), + directory: Directory(_defaultDownloadsFolder), + title: localization.downloadsFolder, + pickText: localization.saveFilesToThisFolder, + ); + + if ((folder ?? '').isNotEmpty) { + _downloadsFolderController.text = folder!; + } + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Text(localization.select), + ), + ), + ], + ), Padding( padding: const EdgeInsets.only(bottom: 10), child: AppDropdownButton( diff --git a/lib/ui/settings/device_settings_vm.dart b/lib/ui/settings/device_settings_vm.dart index dfd3de580..5756d9d05 100644 --- a/lib/ui/settings/device_settings_vm.dart +++ b/lib/ui/settings/device_settings_vm.dart @@ -61,6 +61,7 @@ class DeviceSettingsVM { required this.onEnableTouchEventsChanged, required this.onEnableTooltipsChanged, required this.onEnableFlexibleSearchChanged, + required this.onDownloadsFolderChanged, }); static DeviceSettingsVM fromStore(Store store) { @@ -98,6 +99,9 @@ class DeviceSettingsVM { onTapSelectedChanged: (context, value) async { store.dispatch(UpdateUserPreferences(tapSelectedToEdit: value)); }, + onDownloadsFolderChanged: (context, value) async { + store.dispatch(UpdateUserPreferences(downloadsFolder: value)); + }, onEnableTouchEventsChanged: (context, value) async { store.dispatch(UpdateUserPreferences(enableTouchEvents: value)); store.dispatch(UpdatedSetting()); @@ -221,5 +225,6 @@ class DeviceSettingsVM { final Function(BuildContext, bool) onEnableTooltipsChanged; final Function(BuildContext, bool) onEnableFlexibleSearchChanged; final Function(BuildContext, double) onTextScaleFactorChanged; + final Function(BuildContext, String) onDownloadsFolderChanged; final Future authenticationSupported; } diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index accd23cf8..4c6f3ff28 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -505,6 +505,7 @@ class SettingsSearch extends StatelessWidget { 'show_pdf_preview', 'pdf_preview_location#2022-10-24', 'refresh_data', + 'downloads_folder#2023-10-29' ], [ 'dark_mode', diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 87e0b250a..98057df24 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -107,7 +107,7 @@ Future getAppDownloadDirectory() async { if (!Directory(path).existsSync()) { showErrorDialog( message: AppLocalization.of(navigatorKey.currentContext!)! - .directoryDoesNotExist + .downloadsFolderDoesNotExist .replaceFirst(':value', path)); return null; diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 1fbff80cd..172bc804c 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,10 +18,12 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment -'total_invoiced_quotes': 'Invoiced Quotes', + 'save_files_to_this_folder': 'Save files to this folder', + 'downloads_folder': 'Downloads Folder', + 'total_invoiced_quotes': 'Invoiced Quotes', 'total_invoice_paid_quotes': 'Invoice Paid Quotes', - 'directory_does_not_exist': - 'The download directory does not exist :value', + 'downloads_folder_does_not_exist': + 'The downloads folder does not exist :value', 'user_logged_in_notification': 'User Logged in Notification', 'user_logged_in_notification_help': 'Send an email when logging in from a new location', @@ -109902,20 +109904,27 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['user_logged_in_notification_help'] ?? _localizedValues['en']!['user_logged_in_notification_help']!; - String get directoryDoesNotExist => - _localizedValues[localeCode]!['directory_does_not_exist'] ?? - _localizedValues['en']!['directory_does_not_exist']!; + String get downloadsFolderDoesNotExist => + _localizedValues[localeCode]!['downloads_folder_does_not_exist'] ?? + _localizedValues['en']!['downloads_folder_does_not_exist']!; -String get totalInvoicedQuotes => + String get totalInvoicedQuotes => _localizedValues[localeCode]!['total_invoiced_quotes'] ?? _localizedValues['en']!['total_invoiced_quotes']!; -String get totalInvoicePaidQuotes => + String get totalInvoicePaidQuotes => _localizedValues[localeCode]!['total_invoice_paid_quotes'] ?? _localizedValues['en']!['total_invoice_paid_quotes']!; - - // STARTER: lang field - do not remove comment + String get downloadsFolder => + _localizedValues[localeCode]!['downloads_folder'] ?? + _localizedValues['en']!['downloads_folder']!; + +String get saveFilesToThisFolder => + _localizedValues[localeCode]!['save_files_to_this_folder'] ?? + _localizedValues['en']!['save_files_to_this_folder']!; + + // STARTER: lang field - do not remove comment String lookup(String? key) { final lookupKey = toSnakeCase(key); diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 4ffdf9dae..7acc0400d 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -88,6 +88,7 @@ dependencies: # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 + filesystem_picker: ^4.0.0 dependency_overrides: intl: any diff --git a/pubspec.lock b/pubspec.lock index 0044dfe05..6d4e2e58c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -386,6 +386,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + filesystem_picker: + dependency: "direct main" + description: + name: filesystem_picker + sha256: "37ab68968420c2073b68e002cae786d00ef1cfe18bd2b7255640338a0c47aa9a" + url: "https://pub.dev" + source: hosted + version: "4.0.0" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 530fd9779..209d8d69e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,7 @@ dependencies: # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 + filesystem_picker: ^4.0.0 dependency_overrides: intl: any