Feature Request: Support for setting download location per-device in Windows App #563

This commit is contained in:
Hillel Coren 2023-10-29 09:41:05 +02:00
parent 16ccd02ad8
commit ec2b87933a
10 changed files with 121 additions and 11 deletions

View File

@ -188,6 +188,7 @@ class UpdateUserPreferences implements PersistPrefs {
this.enableTooltips, this.enableTooltips,
this.flexibleSearch, this.flexibleSearch,
this.enableNativeBrowser, this.enableNativeBrowser,
this.downloadsFolder,
this.statementIncludes, this.statementIncludes,
}); });
@ -219,6 +220,7 @@ class UpdateUserPreferences implements PersistPrefs {
final bool? enableTooltips; final bool? enableTooltips;
final bool? flexibleSearch; final bool? flexibleSearch;
final bool? enableNativeBrowser; final bool? enableNativeBrowser;
final String? downloadsFolder;
final BuiltList<String>? statementIncludes; final BuiltList<String>? statementIncludes;
} }

View File

@ -93,6 +93,7 @@ PrefState prefReducer(
longPressReducer(state.longPressSelectionIsDefault, action) longPressReducer(state.longPressSelectionIsDefault, action)
..tapSelectedToEdit = ..tapSelectedToEdit =
tapSelectedToEditReducer(state.tapSelectedToEdit, action) tapSelectedToEditReducer(state.tapSelectedToEdit, action)
..donwloadsFolder = downloadsFolderReducer(state.donwloadsFolder, action)
..requireAuthentication = ..requireAuthentication =
requireAuthenticationReducer(state.requireAuthentication, action) requireAuthenticationReducer(state.requireAuthentication, action)
..colorTheme = colorThemeReducer(state.colorTheme, action) ..colorTheme = colorThemeReducer(state.colorTheme, action)
@ -412,6 +413,12 @@ Reducer<bool> tapSelectedToEditReducer = combineReducers([
}), }),
]); ]);
Reducer<String> downloadsFolderReducer = combineReducers([
TypedReducer<String, UpdateUserPreferences>((downloadsFolder, action) {
return action.downloadsFolder ?? downloadsFolder;
}),
]);
Reducer<bool> isPreviewVisibleReducer = combineReducers([ Reducer<bool> isPreviewVisibleReducer = combineReducers([
TypedReducer<bool, TogglePreviewSidebar>((value, action) { TypedReducer<bool, TogglePreviewSidebar>((value, action) {
return !value; return !value;

View File

@ -1,4 +1,7 @@
// Flutter imports: // Flutter imports:
import 'dart:io';
import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' hide LiveText; 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_redux/flutter_redux.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart';
import 'package:invoiceninja_flutter/redux/company/company_selectors.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:invoiceninja_flutter/utils/formatting.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:timeago/timeago.dart' as timeago; import 'package:timeago/timeago.dart' as timeago;
@ -50,6 +55,11 @@ class _DeviceSettingsState extends State<DeviceSettings>
TabController? _controller; TabController? _controller;
FocusScopeNode? _focusNode; FocusScopeNode? _focusNode;
String _defaultDownloadsFolder = '';
final _downloadsFolderController = TextEditingController();
List<TextEditingController> _controllers = [];
@override @override
void initState() { void initState() {
@ -61,6 +71,37 @@ class _DeviceSettingsState extends State<DeviceSettings>
_controller!.addListener(_onTabChanged); _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() { void _onTabChanged() {
final store = StoreProvider.of<AppState>(context); final store = StoreProvider.of<AppState>(context);
store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index)); store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index));
@ -226,6 +267,41 @@ class _DeviceSettingsState extends State<DeviceSettings>
), ),
FormCard( FormCard(
children: <Widget>[ children: <Widget>[
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(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: AppDropdownButton<double>( child: AppDropdownButton<double>(

View File

@ -61,6 +61,7 @@ class DeviceSettingsVM {
required this.onEnableTouchEventsChanged, required this.onEnableTouchEventsChanged,
required this.onEnableTooltipsChanged, required this.onEnableTooltipsChanged,
required this.onEnableFlexibleSearchChanged, required this.onEnableFlexibleSearchChanged,
required this.onDownloadsFolderChanged,
}); });
static DeviceSettingsVM fromStore(Store<AppState> store) { static DeviceSettingsVM fromStore(Store<AppState> store) {
@ -98,6 +99,9 @@ class DeviceSettingsVM {
onTapSelectedChanged: (context, value) async { onTapSelectedChanged: (context, value) async {
store.dispatch(UpdateUserPreferences(tapSelectedToEdit: value)); store.dispatch(UpdateUserPreferences(tapSelectedToEdit: value));
}, },
onDownloadsFolderChanged: (context, value) async {
store.dispatch(UpdateUserPreferences(downloadsFolder: value));
},
onEnableTouchEventsChanged: (context, value) async { onEnableTouchEventsChanged: (context, value) async {
store.dispatch(UpdateUserPreferences(enableTouchEvents: value)); store.dispatch(UpdateUserPreferences(enableTouchEvents: value));
store.dispatch(UpdatedSetting()); store.dispatch(UpdatedSetting());
@ -221,5 +225,6 @@ class DeviceSettingsVM {
final Function(BuildContext, bool) onEnableTooltipsChanged; final Function(BuildContext, bool) onEnableTooltipsChanged;
final Function(BuildContext, bool) onEnableFlexibleSearchChanged; final Function(BuildContext, bool) onEnableFlexibleSearchChanged;
final Function(BuildContext, double) onTextScaleFactorChanged; final Function(BuildContext, double) onTextScaleFactorChanged;
final Function(BuildContext, String) onDownloadsFolderChanged;
final Future<bool> authenticationSupported; final Future<bool> authenticationSupported;
} }

View File

@ -505,6 +505,7 @@ class SettingsSearch extends StatelessWidget {
'show_pdf_preview', 'show_pdf_preview',
'pdf_preview_location#2022-10-24', 'pdf_preview_location#2022-10-24',
'refresh_data', 'refresh_data',
'downloads_folder#2023-10-29'
], ],
[ [
'dark_mode', 'dark_mode',

View File

@ -107,7 +107,7 @@ Future<String?> getAppDownloadDirectory() async {
if (!Directory(path).existsSync()) { if (!Directory(path).existsSync()) {
showErrorDialog( showErrorDialog(
message: AppLocalization.of(navigatorKey.currentContext!)! message: AppLocalization.of(navigatorKey.currentContext!)!
.directoryDoesNotExist .downloadsFolderDoesNotExist
.replaceFirst(':value', path)); .replaceFirst(':value', path));
return null; return null;

View File

@ -18,10 +18,12 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = { static final Map<String, Map<String, String>> _localizedValues = {
'en': { 'en': {
// STARTER: lang key - do not remove comment // 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', 'total_invoice_paid_quotes': 'Invoice Paid Quotes',
'directory_does_not_exist': 'downloads_folder_does_not_exist':
'The download directory does not exist :value', 'The downloads folder does not exist :value',
'user_logged_in_notification': 'User Logged in Notification', 'user_logged_in_notification': 'User Logged in Notification',
'user_logged_in_notification_help': 'user_logged_in_notification_help':
'Send an email when logging in from a new location', '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[localeCode]!['user_logged_in_notification_help'] ??
_localizedValues['en']!['user_logged_in_notification_help']!; _localizedValues['en']!['user_logged_in_notification_help']!;
String get directoryDoesNotExist => String get downloadsFolderDoesNotExist =>
_localizedValues[localeCode]!['directory_does_not_exist'] ?? _localizedValues[localeCode]!['downloads_folder_does_not_exist'] ??
_localizedValues['en']!['directory_does_not_exist']!; _localizedValues['en']!['downloads_folder_does_not_exist']!;
String get totalInvoicedQuotes => String get totalInvoicedQuotes =>
_localizedValues[localeCode]!['total_invoiced_quotes'] ?? _localizedValues[localeCode]!['total_invoiced_quotes'] ??
_localizedValues['en']!['total_invoiced_quotes']!; _localizedValues['en']!['total_invoiced_quotes']!;
String get totalInvoicePaidQuotes => String get totalInvoicePaidQuotes =>
_localizedValues[localeCode]!['total_invoice_paid_quotes'] ?? _localizedValues[localeCode]!['total_invoice_paid_quotes'] ??
_localizedValues['en']!['total_invoice_paid_quotes']!; _localizedValues['en']!['total_invoice_paid_quotes']!;
String get downloadsFolder =>
_localizedValues[localeCode]!['downloads_folder'] ??
_localizedValues['en']!['downloads_folder']!;
// STARTER: lang field - do not remove comment 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) { String lookup(String? key) {
final lookupKey = toSnakeCase(key); final lookupKey = toSnakeCase(key);

View File

@ -88,6 +88,7 @@ dependencies:
# quick_actions: ^0.2.1 # quick_actions: ^0.2.1
# idb_shim: ^1.11.1+1 # idb_shim: ^1.11.1+1
collection: ^1.15.0-nullsafety.4 collection: ^1.15.0-nullsafety.4
filesystem_picker: ^4.0.0
dependency_overrides: dependency_overrides:
intl: any intl: any

View File

@ -386,6 +386,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+1" 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: fixnum:
dependency: transitive dependency: transitive
description: description:

View File

@ -94,6 +94,7 @@ dependencies:
# quick_actions: ^0.2.1 # quick_actions: ^0.2.1
# idb_shim: ^1.11.1+1 # idb_shim: ^1.11.1+1
collection: ^1.15.0-nullsafety.4 collection: ^1.15.0-nullsafety.4
filesystem_picker: ^4.0.0
dependency_overrides: dependency_overrides:
intl: any intl: any