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/model/settings.dart b/lib/model/settings.dart new file mode 100644 index 0000000..14f7f15 --- /dev/null +++ b/lib/model/settings.dart @@ -0,0 +1,39 @@ +import '../assets/constants.dart' as constants; + +class Settings { + final String exportDirectoryPath; + final bool alwaysExportEnabled; + + Settings._({ + required this.exportDirectoryPath, + required this.alwaysExportEnabled, + }); + + Map toJson() { + 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, 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 86fa41a..45c71cb 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(); + 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 .whenComplete(() => checkStoragePermission()), trailing: Icon(Icons.arrow_forward_ios_rounded), @@ -63,7 +73,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, ), @@ -73,10 +83,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, + ), ], ), ), @@ -84,23 +123,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 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 new file mode 100644 index 0000000..9b3c981 --- /dev/null +++ b/lib/service/settings_provider.dart @@ -0,0 +1,24 @@ +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); + Storage.saveSettings(_settings); + if (!silent) notifyListeners(); + } + + 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 8ae8bac..8427a85 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -4,22 +4,49 @@ 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 Settings _currentSettings = Settings.defaults(); 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; + _currentSettings = Settings.fromJson(json); + return _currentSettings; + } else { + final settings = Settings.defaults(); + saveSettings(settings); + return settings; + } + } + + static Future saveSettings(Settings settings) { + final json = jsonEncode(settings.toJson()); + _currentSettings = settings; + return _prefs.setString(_settingsKey, json); + } + static List loadCollections() { final jsonString = _prefs.getString(_collectionsKey) ?? '[]'; final jsonList = jsonDecode(jsonString) as List; @@ -39,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) { @@ -156,15 +185,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 +200,22 @@ 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( + 'BookmarkStorage not initialized. Call initialize() first.', + ); + } + return _prefsWithCache!; + } }