From ff1b102047fceaf9f887ec45d9897b5486567e5b Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 16:41:16 +0100 Subject: [PATCH 1/7] fixed visual bug --- lib/service/notifying.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/service/notifying.dart b/lib/service/notifying.dart index 4870c2b..4a3874a 100644 --- a/lib/service/notifying.dart +++ b/lib/service/notifying.dart @@ -12,7 +12,7 @@ class Notifying { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.error, + backgroundColor: isError ? Theme.of(context).colorScheme.error : null, content: SizedBox( height: 30, child: Row( @@ -29,7 +29,7 @@ class Notifying { ScaffoldMessenger.of(context).hideCurrentSnackBar(), icon: Icon( Icons.close_rounded, - color: Theme.of(context).colorScheme.onError, + color: isError ? Theme.of(context).colorScheme.onError : null, ), ), ], From 100b86d3f9b865d923ae4ee4f50f981930303d52 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 16:41:29 +0100 Subject: [PATCH 2/7] added list tile content padding --- lib/theme.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/theme.dart b/lib/theme.dart index 47289ac..4d69664 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -22,5 +22,6 @@ ThemeData _baseTheme(ColorScheme scheme) => shape: RoundedRectangleBorder( borderRadius: BorderRadiusGeometry.circular(12), ), + contentPadding: EdgeInsetsDirectional.only(start: 16.0, end: 24.0), ), ); From 214ae08bb9aac8adcd0a2b1b30b49db9eb0317cb Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 16:41:41 +0100 Subject: [PATCH 3/7] fixed wrong return value --- lib/service/json_file_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/json_file_service.dart b/lib/service/json_file_service.dart index 95d1162..dff109b 100644 --- a/lib/service/json_file_service.dart +++ b/lib/service/json_file_service.dart @@ -32,7 +32,7 @@ class JsonFileService { } catch (e) { return false; } - return false; + return true; } static Future<({List collections, List bookmarks})> From 336be6cb723aa7a2697854b9d36b2c144f373bc2 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 16:41:53 +0100 Subject: [PATCH 4/7] visually changed settings page --- lib/pages/settings_page.dart | 100 +++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index babf159..0bf1b5f 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -17,55 +17,89 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + bool storagePermissionIsGranted = false; + final tileSpacing = 16.0; + + @override + void initState() { + PermissionService.storagePermissionStatus.then((value) { + storagePermissionIsGranted = value.isGranted; + if (context.mounted) setState(() {}); + }); + super.initState(); + } + @override Widget build(BuildContext context) { + final titlePadding = Theme.of(context).listTileTheme.contentPadding!; + checkStoragePermission; return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), - body: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context)!.appData, - style: Theme.of(context).textTheme.titleLarge, - ), - ElevatedButton( - onPressed: () => onActivateJsonImportPressed(), - child: Text(AppLocalizations.of(context)!.import), - ), - ElevatedButton( - onPressed: () => onActivateJsonExportPressed(), - child: Text(AppLocalizations.of(context)!.export), - ), - ], + body: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: titlePadding, + child: Text( + AppLocalizations.of(context)!.appData, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + SizedBox(height: tileSpacing), + ListTile( + title: Text('Grant storage permisson'), + subtitle: Text( + 'For app-data settings to work, you need to grant the app permissions to manage internal storage.', + ), + onTap: () => PermissionService.requestStoragePermission, + trailing: Icon(Icons.arrow_forward_ios_rounded), + enabled: !storagePermissionIsGranted, + ), + SizedBox(height: tileSpacing), + ListTile( + title: Text(AppLocalizations.of(context)!.import), + subtitle: Text('Import app-data from a json file.'), + onTap: () => onActivateJsonImportPressed(), + trailing: Icon(Icons.arrow_forward_ios_rounded), + enabled: storagePermissionIsGranted, + ), + SizedBox(height: tileSpacing), + ListTile( + title: Text(AppLocalizations.of(context)!.export), + subtitle: Text( + 'Export app-data to a json file in the selected directory.', + ), + onTap: () => onActivateJsonExportPressed(), + trailing: Icon(Icons.arrow_forward_ios_rounded), + enabled: storagePermissionIsGranted, + ), + ], + ), ), ), ); } void onActivateJsonExportPressed() async { - if (!await checkStoragePermission) return; - + if (!await PermissionService.storagePermissionStatus.isGranted) return; Storage.exportToJsonFile().then(showExportInfo); } void onActivateJsonImportPressed() async { - if (!await checkStoragePermission) return; + if (!await PermissionService.storagePermissionStatus.isGranted) return; Storage.importFromJsonFile().then(showImportInfo); } - Future get checkStoragePermission async { - if (!(await PermissionService.requestStoragePermission).isGranted) { - if (mounted) { - Notifying.showErrorSnackbar( - context, - AppLocalizations.of(context)!.errorStoragePermisson, - ); - return false; + Future get checkStoragePermission async { + PermissionService.storagePermissionStatus.then((value) { + storagePermissionIsGranted = value.isGranted; + if (context.mounted && value.isGranted != storagePermissionIsGranted) { + setState(() {}); } - } - return true; + }); } void showExportInfo(bool success) => Notifying.showSnackbar( @@ -73,7 +107,7 @@ class _SettingsPageState extends State { text: success ? AppLocalizations.of(context)!.exportSuccess : AppLocalizations.of(context)!.exportFailed, - isError: success, + isError: !success, ); void showImportInfo(bool success) => Notifying.showSnackbar( @@ -81,6 +115,6 @@ class _SettingsPageState extends State { text: success ? AppLocalizations.of(context)!.importSuccess : AppLocalizations.of(context)!.importFailed, - isError: success, + isError: !success, ); } From 5c44574949092eb4e1a599f95920d4c132948c3e Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 16:49:26 +0100 Subject: [PATCH 5/7] created settings model --- lib/model/settings.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/model/settings.dart diff --git a/lib/model/settings.dart b/lib/model/settings.dart new file mode 100644 index 0000000..b236cfd --- /dev/null +++ b/lib/model/settings.dart @@ -0,0 +1,29 @@ +import '../assets/constants.dart' as constants; + +class Settings { + final String exportDirectoryPath; + + Settings._({required this.exportDirectoryPath}); + + Map toJson() { + return {'exportDirectoryPath': exportDirectoryPath}; + } + + factory Settings.fromJson(Map json) { + return Settings._( + exportDirectoryPath: json['exportDirectoryPath'] as String, + ); + } + + factory Settings.defaults() { + return Settings._( + exportDirectoryPath: constants.defaultAndroidExportDirectory, + ); + } + + Settings copyWith({String? exportDirectoryPath}) { + return Settings._( + exportDirectoryPath: exportDirectoryPath ?? this.exportDirectoryPath, + ); + } +} From cad43c7664aea81bbc82c67de616e0aa45bf7b05 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 17:02:57 +0100 Subject: [PATCH 6/7] added app settings api --- lib/main.dart | 2 ++ lib/service/settings_provider.dart | 20 ++++++++++++++ lib/service/storage.dart | 44 +++++++++++++++++++++++------- 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 lib/service/settings_provider.dart diff --git a/lib/main.dart b/lib/main.dart index 0ce76e9..be1d3d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'pages/collections_list_page.dart'; import 'pages/search_page.dart'; import 'pages/settings_page.dart'; import 'service/search_provider.dart'; +import 'service/settings_provider.dart'; import 'service/shared_link_provider.dart'; import 'service/storage.dart'; import 'service/share_intent_service.dart'; @@ -20,6 +21,7 @@ void main() async { providers: [ ChangeNotifierProvider(create: (_) => SharedLinkProvider()), ChangeNotifierProvider(create: (_) => SearchProvider()), + ChangeNotifierProvider(create: (_) => SettingsProvider()), ], child: const MapsBookmarks(), ), diff --git a/lib/service/settings_provider.dart b/lib/service/settings_provider.dart new file mode 100644 index 0000000..19dcc1c --- /dev/null +++ b/lib/service/settings_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import '../model/settings.dart'; +import 'storage.dart'; + +class SettingsProvider extends ChangeNotifier { + SettingsProvider() : _settings = Storage.loadSettings(); + + Settings _settings; + + Settings get settings => _settings; + + void setExportDirectoryPath(String path, {bool silent = false}) { + _settings = _settings.copyWith(exportDirectoryPath: path); + _saveSettings(); + if (!silent) notifyListeners(); + } + + void _saveSettings() => Storage.saveSettings(_settings); +} diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 8ae8bac..bbe0385 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -4,22 +4,46 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../model/bookmark.dart'; import '../model/collection.dart'; +import '../model/settings.dart'; import 'json_file_service.dart'; class Storage { static const String _bookmarksKey = 'bookmarks'; static const String _collectionsKey = 'collections'; static SharedPreferencesWithCache? _prefsWithCache; + static const String _settingsKey = 'settings'; static const String _statsKey = 'stats'; static Future initialize() async { _prefsWithCache = await SharedPreferencesWithCache.create( cacheOptions: const SharedPreferencesWithCacheOptions( - allowList: {_collectionsKey, _bookmarksKey, _statsKey}, + allowList: { + _collectionsKey, + _bookmarksKey, + _statsKey, + _settingsKey, + }, ), ); } + static Settings loadSettings() { + final jsonString = _prefs.getString(_settingsKey); + if (jsonString != null) { + final json = jsonDecode(jsonString) as Map; + return Settings.fromJson(json); + } else { + final settings = Settings.defaults(); + saveSettings(settings); + return settings; + } + } + + static Future saveSettings(Settings settings) { + final json = jsonEncode(settings.toJson()); + return _prefs.setString(_settingsKey, json); + } + static List loadCollections() { final jsonString = _prefs.getString(_collectionsKey) ?? '[]'; final jsonList = jsonDecode(jsonString) as List; @@ -156,15 +180,6 @@ class Storage { await _prefs.setString(_statsKey, jsonEncode(stats)); } - static SharedPreferencesWithCache get _prefs { - if (_prefsWithCache == null) { - throw StateError( - 'BookmarkStorage not initialized. Call initialize() first.', - ); - } - return _prefsWithCache!; - } - static Future exportToJsonFile() => JsonFileService.exportToJson( collections: loadCollections(), bookmarks: loadBookmarks(), @@ -180,4 +195,13 @@ class Storage { } return false; } + + static SharedPreferencesWithCache get _prefs { + if (_prefsWithCache == null) { + throw StateError( + 'BookmarkStorage not initialized. Call initialize() first.', + ); + } + return _prefsWithCache!; + } } From 83bfdf322bc7338a39d20232033503e273ee1555 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 23 Jan 2026 18:03:46 +0100 Subject: [PATCH 7/7] added persisted app settings --- lib/model/settings.dart | 16 ++++- lib/pages/settings_page.dart | 94 ++++++++++++++++++++++++++---- lib/service/json_file_service.dart | 41 ++++++++----- lib/service/settings_provider.dart | 8 ++- lib/service/storage.dart | 16 ++++- 5 files changed, 143 insertions(+), 32 deletions(-) diff --git a/lib/model/settings.dart b/lib/model/settings.dart index b236cfd..14f7f15 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -2,28 +2,38 @@ import '../assets/constants.dart' as constants; class Settings { final String exportDirectoryPath; + final bool alwaysExportEnabled; - Settings._({required this.exportDirectoryPath}); + Settings._({ + required this.exportDirectoryPath, + required this.alwaysExportEnabled, + }); Map toJson() { - return {'exportDirectoryPath': exportDirectoryPath}; + return { + 'exportDirectoryPath': exportDirectoryPath, + 'alwaysExportEnabled': alwaysExportEnabled, + }; } factory Settings.fromJson(Map json) { return Settings._( exportDirectoryPath: json['exportDirectoryPath'] as String, + alwaysExportEnabled: json['alwaysExportEnabled'] as bool, ); } factory Settings.defaults() { return Settings._( exportDirectoryPath: constants.defaultAndroidExportDirectory, + alwaysExportEnabled: false, ); } - Settings copyWith({String? exportDirectoryPath}) { + Settings copyWith({String? exportDirectoryPath, bool? alwaysExportEnabled}) { return Settings._( exportDirectoryPath: exportDirectoryPath ?? this.exportDirectoryPath, + alwaysExportEnabled: alwaysExportEnabled ?? this.alwaysExportEnabled, ); } } diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 0bf1b5f..afdd3d7 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; import '../l10n/app_localizations.dart'; import '../service/notifying.dart'; import '../service/permission_service.dart'; +import '../service/settings_provider.dart'; import '../service/storage.dart'; class SettingsPage extends StatefulWidget { @@ -29,10 +31,16 @@ class _SettingsPageState extends State { super.initState(); } + // TODO: Localize @override Widget build(BuildContext context) { final titlePadding = Theme.of(context).listTileTheme.contentPadding!; - checkStoragePermission; + checkStoragePermission(); + final alwaysExportEnabled = context + .watch() + .settings + .alwaysExportEnabled; + return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), body: Center( @@ -51,9 +59,11 @@ class _SettingsPageState extends State { SizedBox(height: tileSpacing), ListTile( title: Text('Grant storage permisson'), - subtitle: Text( - 'For app-data settings to work, you need to grant the app permissions to manage internal storage.', - ), + subtitle: storagePermissionIsGranted + ? Text('Storage permission granted') + : Text( + 'For app-data settings to work, you need to grant the app permissions to manage internal storage.', + ), onTap: () => PermissionService.requestStoragePermission, trailing: Icon(Icons.arrow_forward_ios_rounded), enabled: !storagePermissionIsGranted, @@ -62,7 +72,7 @@ class _SettingsPageState extends State { ListTile( title: Text(AppLocalizations.of(context)!.import), subtitle: Text('Import app-data from a json file.'), - onTap: () => onActivateJsonImportPressed(), + onTap: () => onJsonImportPressed(), trailing: Icon(Icons.arrow_forward_ios_rounded), enabled: storagePermissionIsGranted, ), @@ -72,10 +82,39 @@ class _SettingsPageState extends State { subtitle: Text( 'Export app-data to a json file in the selected directory.', ), - onTap: () => onActivateJsonExportPressed(), + onTap: () => onJsonExportPressed(), trailing: Icon(Icons.arrow_forward_ios_rounded), enabled: storagePermissionIsGranted, ), + SizedBox(height: tileSpacing), + ListTile( + title: Text('Always save to file'), + subtitle: Text( + 'Export app data to a directory, every time you make a change', + ), + onTap: () => onAlwaysSaveToJsonPressed(), + trailing: Checkbox( + value: alwaysExportEnabled, + onChanged: (value) { + onAlwaysSaveToJsonPressed(); + }, + ), + enabled: storagePermissionIsGranted, + ), + if (alwaysExportEnabled) SizedBox(height: tileSpacing), + if (alwaysExportEnabled) + ListTile( + title: Text('Change export directory'), + subtitle: Text( + context + .watch() + .settings + .exportDirectoryPath, + ), + onTap: () => onChangeExportDirectoryPressed(), + trailing: Icon(Icons.arrow_forward_ios_rounded), + enabled: storagePermissionIsGranted, + ), ], ), ), @@ -83,23 +122,54 @@ class _SettingsPageState extends State { ); } - void onActivateJsonExportPressed() async { - if (!await PermissionService.storagePermissionStatus.isGranted) return; + void onJsonExportPressed() async { + if (!await checkStoragePermission()) return; Storage.exportToJsonFile().then(showExportInfo); } - void onActivateJsonImportPressed() async { - if (!await PermissionService.storagePermissionStatus.isGranted) return; + void onJsonImportPressed() async { + if (!await checkStoragePermission()) return; Storage.importFromJsonFile().then(showImportInfo); } - Future get checkStoragePermission async { + void onAlwaysSaveToJsonPressed() async { + if (context.read().settings.alwaysExportEnabled) { + context.read().setExportDirectoryPath('', silent: true); + context.read().setAlwaysExportEnabled(false); + return; + } + + if (!await PermissionService.storagePermissionStatus.isGranted) return; + final dir = await Storage.selectDirectoryPath(); + if (dir.isEmpty || !context.mounted) return; + + // ignore: use_build_context_synchronously + context.read().setExportDirectoryPath(dir, silent: true); + + // ignore: use_build_context_synchronously + Storage.saveDataToFile().whenComplete( + // ignore: use_build_context_synchronously + () => context.read().setAlwaysExportEnabled(true), + ); + } + + void onChangeExportDirectoryPressed() async { + if (!await PermissionService.storagePermissionStatus.isGranted) return; + final dir = await Storage.selectDirectoryPath(); + if (dir.isEmpty || !context.mounted) return; + + // ignore: use_build_context_synchronously + context.read().setExportDirectoryPath(dir); + } + + Future checkStoragePermission() async { PermissionService.storagePermissionStatus.then((value) { - storagePermissionIsGranted = value.isGranted; if (context.mounted && value.isGranted != storagePermissionIsGranted) { + storagePermissionIsGranted = value.isGranted; setState(() {}); } }); + return storagePermissionIsGranted; } void showExportInfo(bool success) => Notifying.showSnackbar( diff --git a/lib/service/json_file_service.dart b/lib/service/json_file_service.dart index dff109b..f04892a 100644 --- a/lib/service/json_file_service.dart +++ b/lib/service/json_file_service.dart @@ -14,21 +14,10 @@ class JsonFileService { required List bookmarks, }) async { try { - final dir = await _directoryPath; + final dir = await selectDirectoryPath(); if (dir.isEmpty) return false; - final data = { - 'collections': collections.map((c) => c.toJson()).toList(), - 'bookmarks': bookmarks.map((b) => b.toJson()).toList(), - }; - final json = jsonEncode(data).codeUnits; - final file = XFile.fromData( - Uint8List.fromList(json), - mimeType: 'application/json', - name: constants.jsonFileName, - ); - - file.saveTo('$dir/${constants.jsonFileName}'); + saveDataToFile(collections, bookmarks, dir); } catch (e) { return false; } @@ -65,7 +54,31 @@ class JsonFileService { } } - static Future get _directoryPath async { + static Future saveDataToFile( + List collections, + List bookmarks, + String directory, + ) async { + try { + final data = jsonEncode({ + 'collections': collections.map((c) => c.toJson()).toList(), + 'bookmarks': bookmarks.map((b) => b.toJson()).toList(), + }).codeUnits; + + final file = XFile.fromData( + Uint8List.fromList(data), + mimeType: 'application/json', + name: constants.jsonFileName, + ); + + file.saveTo('$directory/${constants.jsonFileName}'); + } catch (e) { + return false; + } + return true; + } + + static Future selectDirectoryPath() async { if (Platform.isAndroid) { return await getDirectoryPath( initialDirectory: constants.defaultAndroidExportDirectory, diff --git a/lib/service/settings_provider.dart b/lib/service/settings_provider.dart index 19dcc1c..9b3c981 100644 --- a/lib/service/settings_provider.dart +++ b/lib/service/settings_provider.dart @@ -12,9 +12,13 @@ class SettingsProvider extends ChangeNotifier { void setExportDirectoryPath(String path, {bool silent = false}) { _settings = _settings.copyWith(exportDirectoryPath: path); - _saveSettings(); + Storage.saveSettings(_settings); if (!silent) notifyListeners(); } - void _saveSettings() => Storage.saveSettings(_settings); + void setAlwaysExportEnabled(bool enabled, {bool silent = false}) { + _settings = _settings.copyWith(alwaysExportEnabled: enabled); + Storage.saveSettings(_settings); + if (!silent) notifyListeners(); + } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index bbe0385..8427a85 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -13,6 +13,7 @@ class Storage { static SharedPreferencesWithCache? _prefsWithCache; static const String _settingsKey = 'settings'; static const String _statsKey = 'stats'; + static Settings _currentSettings = Settings.defaults(); static Future initialize() async { _prefsWithCache = await SharedPreferencesWithCache.create( @@ -31,7 +32,8 @@ class Storage { final jsonString = _prefs.getString(_settingsKey); if (jsonString != null) { final json = jsonDecode(jsonString) as Map; - return Settings.fromJson(json); + _currentSettings = Settings.fromJson(json); + return _currentSettings; } else { final settings = Settings.defaults(); saveSettings(settings); @@ -41,6 +43,7 @@ class Storage { static Future saveSettings(Settings settings) { final json = jsonEncode(settings.toJson()); + _currentSettings = settings; return _prefs.setString(_settingsKey, json); } @@ -63,11 +66,13 @@ class Storage { static Future saveCollections(List collections) async { final jsonList = collections.map((c) => c.toJson()).toList(); await _prefs.setString(_collectionsKey, jsonEncode(jsonList)); + if (_currentSettings.alwaysExportEnabled) saveDataToFile(); } static Future saveBookmarks(List bookmarks) async { final jsonList = bookmarks.map((b) => b.toJson()).toList(); await _prefs.setString(_bookmarksKey, jsonEncode(jsonList)); + if (_currentSettings.alwaysExportEnabled) saveDataToFile(); } static List loadBookmarksForCollection(int collectionId) { @@ -196,6 +201,15 @@ class Storage { return false; } + static Future selectDirectoryPath() => + JsonFileService.selectDirectoryPath(); + + static Future saveDataToFile() => JsonFileService.saveDataToFile( + loadCollections(), + loadBookmarks(), + _currentSettings.exportDirectoryPath, + ); + static SharedPreferencesWithCache get _prefs { if (_prefsWithCache == null) { throw StateError(