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.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<String>? statementIncludes;
}

View File

@ -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<bool> tapSelectedToEditReducer = combineReducers([
}),
]);
Reducer<String> downloadsFolderReducer = combineReducers([
TypedReducer<String, UpdateUserPreferences>((downloadsFolder, action) {
return action.downloadsFolder ?? downloadsFolder;
}),
]);
Reducer<bool> isPreviewVisibleReducer = combineReducers([
TypedReducer<bool, TogglePreviewSidebar>((value, action) {
return !value;

View File

@ -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<DeviceSettings>
TabController? _controller;
FocusScopeNode? _focusNode;
String _defaultDownloadsFolder = '';
final _downloadsFolderController = TextEditingController();
List<TextEditingController> _controllers = [];
@override
void initState() {
@ -61,6 +71,37 @@ class _DeviceSettingsState extends State<DeviceSettings>
_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<AppState>(context);
store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index));
@ -226,6 +267,41 @@ class _DeviceSettingsState extends State<DeviceSettings>
),
FormCard(
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: const EdgeInsets.only(bottom: 10),
child: AppDropdownButton<double>(

View File

@ -61,6 +61,7 @@ class DeviceSettingsVM {
required this.onEnableTouchEventsChanged,
required this.onEnableTooltipsChanged,
required this.onEnableFlexibleSearchChanged,
required this.onDownloadsFolderChanged,
});
static DeviceSettingsVM fromStore(Store<AppState> 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<bool> authenticationSupported;
}

View File

@ -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',

View File

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

View File

@ -18,10 +18,12 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _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);

View File

@ -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

View File

@ -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:

View File

@ -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