From 6ee0536207cf18e122691b43630e61235f978085 Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Sat, 29 Apr 2023 08:47:12 +0100 Subject: [PATCH 1/9] Improve release checklist --- Release checklist.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Release checklist.md b/Release checklist.md index 475b1f1..fe237ae 100644 --- a/Release checklist.md +++ b/Release checklist.md @@ -16,7 +16,9 @@ - [ ] Copy changelog in to GitHub - [ ] Attach binaries to GitHub - [ ] cp build/app/outputs/flutter-apk/app-release.apk daily_diary.apk - - [ ] cp -r build/linux/x64/release/bundle/ linux/ - - [ ] tar -czvf linux.tar.gz linux/ - - [ ] rm linux/ + - [ ] cp -r build/linux/x64/release/bundle/ Daily-Diary/ + - [ ] tar -czvf linux.tar.gz Daily-Diary/ + - [ ] rm -r Daily-Diary/ - [ ] Publish +- [ ] rm linux.tar.gz daily_diary.apk +- [ ] git checkout main -f From ef3f6229097655a380b6db87dd1a998f3bc6a72a Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Fri, 19 May 2023 23:05:21 +0100 Subject: [PATCH 2/9] Something compiling --- android/app/build.gradle | 2 +- lib/main.dart | 51 +++++++++++++++++++++- lib/path.dart | 21 ++++++--- lib/screens/settings.dart | 65 +++++++++++++++++++++++----- lib/settings_notifier.dart | 3 +- lib/storage.dart | 89 ++++++++++++++++++++++++++++++++------ pubspec.lock | 24 ++++++---- pubspec.yaml | 1 + 8 files changed, 213 insertions(+), 43 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0266b33..6cfd9ad 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -50,7 +50,7 @@ android { defaultConfig { applicationId "com.voklen.daily_diary" - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/main.dart b/lib/main.dart index f049093..9555b07 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,11 +6,58 @@ import 'package:daily_diary/storage.dart'; import 'package:daily_diary/themes.dart'; import 'package:daily_diary/screens/home.dart'; +import 'package:shared_storage/saf.dart'; + // This will be removed when widgets can react to spell check changes bool? startupCheckSpelling; -String? savePath; -String? startupSavePath; +SavePath? savePath; +SavePath? startupSavePath; + +class SavePath { + // Due to the constructors only one can ever be null at any time + const SavePath.normal(String this.path) : uri = null; + const SavePath.android(Uri this.uri) : path = null; + + final String? path; + final Uri? uri; + + bool get isScopedStorage => path == null; + + String get string => isScopedStorage ? uri!.toString() : path!; + + Future getScopedFile(String filename) async { + final file = await getChildFile(filename); + final content = await file.getContentAsString(); + return content!; + } + + void writeScopedFile(String filename, String content) async { + final file = await getChildFile(filename); + file.writeToFileAsString(content: content); + } + + Future scopedExists(String filename) async { + final scopedStorageFile = await getChildFile(filename); + final exists = await scopedStorageFile.exists(); + return exists!; + } + + void deleteScoped(String filename) async { + final file = await getChildFile(filename); + file.delete(); + } + + Future getChildFile(String filename) async { + final file = await findFile(uri!, filename); + if (file != null) { + return file; + } + DocumentFile? createdFile = + await createFile(uri!, mimeType: 'text/plain', displayName: filename); + return createdFile!; + } +} main() async { savePath = await getPath(); diff --git a/lib/path.dart b/lib/path.dart index 26b4172..e253b87 100644 --- a/lib/path.dart +++ b/lib/path.dart @@ -1,25 +1,34 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_storage/saf.dart'; -Future getPath() async { +Future getPath() async { WidgetsFlutterBinding.ensureInitialized(); final preferences = await SharedPreferences.getInstance(); String? path = preferences.getString('save_path'); + bool? isAndroidScoped = preferences.getBool('is_android_scoped'); if (path != null) { - return path; + if (isAndroidScoped == true) { + DocumentFile document = DocumentFile.fromMap(json.decode(path)); + return SavePath.android(document.uri); + } + return SavePath.normal(path); } else { - path = await defaultPath; - preferences.setString('save_path', path); + SavePath path = await defaultPath; + preferences.setString('save_path', path.path!); + preferences.setBool('is_android_scoped', false); return path; } } -Future get defaultPath async { +Future get defaultPath async { final directory = await _directory; - return directory.path; + return SavePath.normal(directory.path); } Future get _directory async { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 7b8f733..72aaa98 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; import 'package:daily_diary/main.dart'; import 'package:daily_diary/path.dart'; @@ -338,14 +340,16 @@ class SavePathSetting extends StatefulWidget implements SettingTile { @override Future newDefault() async { - await setSavePath(); + await resetSavePath(); return const SavePathSetting(); } - Future setSavePath() async { + Future resetSavePath() async { savePath = await defaultPath; final preferences = await SharedPreferences.getInstance(); - preferences.setString('save_path', await defaultPath); + SavePath path = await defaultPath; + preferences.setString('save_path', path.path!); + preferences.setBool('is_android_scoped', false); } @override @@ -353,21 +357,60 @@ class SavePathSetting extends StatefulWidget implements SettingTile { } class _SavePathSettingState extends State { - _selectNewPath() async { - // Load SharedPreferences while user is picking a path - final preferencesFuture = SharedPreferences.getInstance(); - final path = await FilePicker.platform.getDirectoryPath(); - final preferences = await preferencesFuture; + void _selectNewPath() async { + final path = await _askForPath(); if (path == null) { - // if the user aborted the dialog or if the folder path couldn't be resolved. + // The user aborted the dialog or the folder path couldn't be resolved. return; } - preferences.setString('save_path', path); + setState(() { savePath = path; }); } + Future _askForPath() async { + if (Platform.isAndroid) { + return _askForPathAndroid(); + } + // Load SharedPreferences while user is picking a path + final preferencesFuture = SharedPreferences.getInstance(); + String? path = await FilePicker.platform.getDirectoryPath(); + if (path == null) { + return null; + } + + final preferences = await preferencesFuture; + preferences.setString('save_path', path); + preferences.setBool('is_android_scoped', false); + return SavePath.normal(path); + } + + Future _askForPathAndroid() async { + // Load SharedPreferences while user is picking a path + final preferencesFuture = SharedPreferences.getInstance(); + // Remove previous permissions + if (savePath != null && savePath!.uri != null) { + final previousPath = savePath!.uri!; + saf.releasePersistableUriPermission(previousPath); + } + + // Ask user for path and permissions + Uri? uri; + while (uri == null) { + uri = await saf.openDocumentTree(); + } + + // Only null before Android API 21, but this project is API 21+ + final asDocumentFile = await uri.toDocumentFile(); + Map asMap = asDocumentFile!.toMap(); + String asString = json.encode(asMap); + final preferences = await preferencesFuture; + preferences.setString('save_path', asString); + preferences.setBool('is_android_scoped', true); + return SavePath.android(uri); + } + @override Widget build(BuildContext context) { return Column( @@ -382,7 +425,7 @@ class _SavePathSettingState extends State { ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: TextField( - controller: TextEditingController(text: savePath), + controller: TextEditingController(text: savePath!.string), enabled: false, style: Theme.of(context).textTheme.bodyMedium, ), diff --git a/lib/settings_notifier.dart b/lib/settings_notifier.dart index e1dc77e..63bdb21 100644 --- a/lib/settings_notifier.dart +++ b/lib/settings_notifier.dart @@ -1,3 +1,4 @@ +import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; import 'package:daily_diary/storage.dart'; @@ -10,7 +11,7 @@ class Settings { } class SettingsNotifier extends ValueNotifier { - SettingsNotifier(String savePath) + SettingsNotifier(SavePath savePath) : storage = SettingsStorage(savePath), super(Settings()); diff --git a/lib/storage.dart b/lib/storage.dart index dade813..66091a7 100644 --- a/lib/storage.dart +++ b/lib/storage.dart @@ -1,30 +1,49 @@ import 'dart:async'; import 'dart:io'; +import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:shared_storage/saf.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; import 'package:toml/toml.dart'; class DiaryStorage { DiaryStorage(this.path); - final String path; + final SavePath path; DateTime date = DateTime.now(); + String get isoDate => date.toIso8601String().substring(0, 10); + File get file { - String isoDate = date.toIso8601String().substring(0, 10); return File('$path/$isoDate.txt'); } Future readFile() async { try { + if (path.isScopedStorage) { + return path.getScopedFile('$isoDate.txt'); + } return await file.readAsString(); } catch (error) { return ''; } } - void writeFile(String text) { + void writeFile(String text) async { + if (path.isScopedStorage) { + if (text.isNotEmpty) { + path.writeScopedFile('$isoDate.txt', text); + return; + } + if (await path.scopedExists('$isoDate.txt')) { + path.deleteScoped('$isoDate.txt'); + return; + } + return; + } + if (text.isNotEmpty) { file.writeAsStringSync(text); return; @@ -43,12 +62,12 @@ class DiaryStorage { class SettingsStorage { SettingsStorage(this.path); - final String path; + final SavePath path; late var settingsMap = _getMap(); Future> _getMap() async { try { - final file = await _document; + TomlDocument file = await _document; return file.toMap(); } on FileSystemException { return {}; @@ -56,10 +75,14 @@ class SettingsStorage { } String get _file { - return '$path/config.toml'; + return '${path.path}/config.toml'; } Future get _document async { + if (path.isScopedStorage) { + String content = await path.getScopedFile('config.toml'); + return TomlDocument.parse(content); + } return TomlDocument.load(_file); } @@ -134,28 +157,57 @@ class SettingsStorage { map[key] = value; settingsMap = Future(() => map); - TomlDocument asToml = TomlDocument.fromMap(map); - await File(_file).writeAsString(asToml.toString()); + //TODO + String asToml = TomlDocument.fromMap(map).toString(); + + if (path.isScopedStorage) { + DocumentFile file = await path.getChildFile('config.toml'); + await file.writeToFileAsString(content: asToml); + } else { + await File(_file).writeAsString(asToml); + } } } class PreviousEntriesStorage { const PreviousEntriesStorage(this.path); - final String path; + final SavePath path; Future> getFiles() async { - final directory = Directory(path); + if (path.isScopedStorage) { + return _getFilesScopedStorage(path.uri!); + } + + final directory = Directory(path.path!); final files = directory.list(); - final filesAsDateTime = files.map(toFilename); + final filesAsDateTime = files.map(toFilenameFromFileEntity); final filesWithoutNull = filesAsDateTime.where((s) => s != null).cast(); final list = await filesWithoutNull.toList(); return list.reversed.toList(); } - DateTime? toFilename(FileSystemEntity file) { - String path = file.path; + Future> _getFilesScopedStorage(Uri uri) async { + if (await canRead(uri) == true) { + //TODO handle lack of permissions + } + final files = listFiles(uri, columns: [DocumentFileColumn.displayName]); + final filesAsDateTime = files.map(toFilenameFromDocumentFile); + final filesWithoutNull = + filesAsDateTime.where((s) => s != null).cast(); + return filesWithoutNull.toList(); + } + + DateTime? toFilenameFromFileEntity(FileSystemEntity file) { + return toFilename(file.path); + } + + DateTime? toFilenameFromDocumentFile(DocumentFile file) { + return toFilename(file.name!); + } + + DateTime? toFilename(String path) { int filenameStart = path.lastIndexOf('/') + 1; int filenameEnd = path.length - 4; String isoDate = path.substring(filenameStart, filenameEnd); @@ -172,10 +224,13 @@ class PreviousEntryStorage { const PreviousEntryStorage(this.filename, this.path); final String filename; - final String path; + final SavePath path; Future readFile() async { try { + if (path.isScopedStorage) { + return await _readFileAndroid(path.uri!); + } final file = File('$path/$filename.txt'); final contents = await file.readAsString(); return contents; @@ -183,4 +238,10 @@ class PreviousEntryStorage { return ""; } } + + Future _readFileAndroid(Uri uri) async { + DocumentFile? child = await findFile(uri, filename); + String? contents = await child!.getContentAsString(); + return contents!; + } } diff --git a/pubspec.lock b/pubspec.lock index 7dbf915..2396ed9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: e6c7ad8e572379df86ea64ef0a5395889fba3954411d47ca021b888d79f8e798 + sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff url: "https://pub.dev" source: hosted - version: "5.2.11" + version: "5.2.10" flutter: dependency: "direct main" description: flutter @@ -196,10 +196,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.25" path_provider_foundation: dependency: transitive description: @@ -276,10 +276,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" shared_preferences_foundation: dependency: transitive description: @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + shared_storage: + dependency: "direct main" + description: + name: shared_storage + sha256: "2761c1f6562d7cfb7626982d3fc7ce0bb1d9bf86ff7db2f6d77ee078d8f57de9" + url: "https://pub.dev" + source: hosted + version: "0.7.1" sky_engine: dependency: transitive description: flutter @@ -393,10 +401,10 @@ packages: dependency: transitive description: name: win32 - sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a3a1564..14848b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: shared_preferences: ^2.0.15 file_picker: ^5.2.4 flutter_window_close: ^0.2.2 + shared_storage: ^0.7.1 dev_dependencies: flutter_test: From 15715e01eea854c05cca98ef536e7c12e2eedc8e Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Sat, 20 May 2023 09:49:11 +0100 Subject: [PATCH 3/9] Fix file accessing --- lib/main.dart | 1 + lib/screens/previous_entries.dart | 3 ++- lib/storage.dart | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9555b07..d01bf4a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,6 +51,7 @@ class SavePath { Future getChildFile(String filename) async { final file = await findFile(uri!, filename); if (file != null) { + print(await file.getContentAsString()); return file; } DocumentFile? createdFile = diff --git a/lib/screens/previous_entries.dart b/lib/screens/previous_entries.dart index 7e5e878..78d8660 100644 --- a/lib/screens/previous_entries.dart +++ b/lib/screens/previous_entries.dart @@ -53,7 +53,8 @@ class PreviousEntry extends StatelessWidget { context, MaterialPageRoute( builder: (context) { - String filename = date.toIso8601String().substring(0, 10); + String isoDate = date.toIso8601String().substring(0, 10); + String filename = '$isoDate.txt'; final storage = PreviousEntryStorage(filename, savePath!); return ViewOnlyScreen(title: humanDate, storage: storage); }, diff --git a/lib/storage.dart b/lib/storage.dart index 66091a7..bec15a9 100644 --- a/lib/storage.dart +++ b/lib/storage.dart @@ -229,9 +229,9 @@ class PreviousEntryStorage { Future readFile() async { try { if (path.isScopedStorage) { - return await _readFileAndroid(path.uri!); + return await _readFileAndroid(); } - final file = File('$path/$filename.txt'); + final file = File('$path/$filename'); final contents = await file.readAsString(); return contents; } catch (error) { @@ -239,9 +239,9 @@ class PreviousEntryStorage { } } - Future _readFileAndroid(Uri uri) async { - DocumentFile? child = await findFile(uri, filename); - String? contents = await child!.getContentAsString(); + Future _readFileAndroid() async { + DocumentFile? child = await path.getChildFile(filename); + String? contents = await child.getContentAsString(); return contents!; } } From 59fcf8fdcfee840e7274217881bf81fe5d5a007f Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Sat, 20 May 2023 15:12:38 +0100 Subject: [PATCH 4/9] Fix tests --- lib/main.dart | 1 - lib/storage.dart | 3 +-- test/storage/diary_storage_test.dart | 5 +++-- test/storage/settings_storage_test.dart | 3 ++- test/widget_test.dart | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d01bf4a..9555b07 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,7 +51,6 @@ class SavePath { Future getChildFile(String filename) async { final file = await findFile(uri!, filename); if (file != null) { - print(await file.getContentAsString()); return file; } DocumentFile? createdFile = diff --git a/lib/storage.dart b/lib/storage.dart index bec15a9..c468552 100644 --- a/lib/storage.dart +++ b/lib/storage.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:shared_storage/saf.dart'; -import 'package:shared_storage/shared_storage.dart' as saf; import 'package:toml/toml.dart'; class DiaryStorage { @@ -17,7 +16,7 @@ class DiaryStorage { String get isoDate => date.toIso8601String().substring(0, 10); File get file { - return File('$path/$isoDate.txt'); + return File('${path.path}/$isoDate.txt'); } Future readFile() async { diff --git a/test/storage/diary_storage_test.dart b/test/storage/diary_storage_test.dart index 8e56d3f..3ac13d6 100644 --- a/test/storage/diary_storage_test.dart +++ b/test/storage/diary_storage_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:daily_diary/main.dart'; import 'package:daily_diary/storage.dart'; main() { @@ -12,7 +13,7 @@ main() { }); test('Normal', () async { - final storage = DiaryStorage('test_data/'); + final storage = DiaryStorage(const SavePath.normal('test_data/')); const testText = 'This is a test diary\n a newline here\nwow, another'; storage.writeFile(testText); String result = await storage.readFile(); @@ -21,7 +22,7 @@ main() { }); test('Unicode', () async { - final storage = DiaryStorage('test_data/'); + final storage = DiaryStorage(const SavePath.normal('test_data/')); const testText = 'This is a اختبر diary\n a newline here\nа вот еще один'; storage.writeFile(testText); String result = await storage.readFile(); diff --git a/test/storage/settings_storage_test.dart b/test/storage/settings_storage_test.dart index f20da83..741d17f 100644 --- a/test/storage/settings_storage_test.dart +++ b/test/storage/settings_storage_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:daily_diary/main.dart'; import 'package:daily_diary/storage.dart'; main() { @@ -13,7 +14,7 @@ main() { }); test('Normal', () async { - final storage = SettingsStorage(testDirectory.path); + final storage = SettingsStorage(SavePath.normal(testDirectory.path)); await storage.setTheme(ThemeMode.dark); await storage.setFontSize(42); diff --git a/test/widget_test.dart b/test/widget_test.dart index 01340a0..89acc89 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,7 +5,7 @@ import 'package:daily_diary/main.dart'; import 'package:daily_diary/screens/settings.dart'; main() { - savePath = ""; + savePath = const SavePath.normal(''); testWidgets('Navigation', (WidgetTester tester) async { await tester.pumpWidget(const App()); From c583b23e450733beddfbe0d08b3f7c41c96df722 Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Sat, 20 May 2023 23:34:05 +0100 Subject: [PATCH 5/9] Refactor --- lib/main.dart | 45 -------------------- lib/path.dart | 56 ++++++++++++++++++++++--- lib/screens/settings.dart | 5 +-- lib/settings_notifier.dart | 2 +- lib/storage.dart | 3 +- test/storage/diary_storage_test.dart | 2 +- test/storage/settings_storage_test.dart | 2 +- test/widget_test.dart | 1 + 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9555b07..a03edb7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,51 +14,6 @@ bool? startupCheckSpelling; SavePath? savePath; SavePath? startupSavePath; -class SavePath { - // Due to the constructors only one can ever be null at any time - const SavePath.normal(String this.path) : uri = null; - const SavePath.android(Uri this.uri) : path = null; - - final String? path; - final Uri? uri; - - bool get isScopedStorage => path == null; - - String get string => isScopedStorage ? uri!.toString() : path!; - - Future getScopedFile(String filename) async { - final file = await getChildFile(filename); - final content = await file.getContentAsString(); - return content!; - } - - void writeScopedFile(String filename, String content) async { - final file = await getChildFile(filename); - file.writeToFileAsString(content: content); - } - - Future scopedExists(String filename) async { - final scopedStorageFile = await getChildFile(filename); - final exists = await scopedStorageFile.exists(); - return exists!; - } - - void deleteScoped(String filename) async { - final file = await getChildFile(filename); - file.delete(); - } - - Future getChildFile(String filename) async { - final file = await findFile(uri!, filename); - if (file != null) { - return file; - } - DocumentFile? createdFile = - await createFile(uri!, mimeType: 'text/plain', displayName: filename); - return createdFile!; - } -} - main() async { savePath = await getPath(); startupSavePath = savePath; diff --git a/lib/path.dart b/lib/path.dart index e253b87..799651d 100644 --- a/lib/path.dart +++ b/lib/path.dart @@ -1,12 +1,56 @@ import 'dart:convert'; import 'dart:io'; -import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_storage/saf.dart'; +class SavePath { + // Due to the constructors only one can ever be null at any time + const SavePath.normal(String this.path) : uri = null; + const SavePath.android(Uri this.uri) : path = null; + + final String? path; + final Uri? uri; + + bool get isScopedStorage => path == null; + + String get string => isScopedStorage ? uri!.toString() : path!; + + Future getScopedFile(String filename) async { + final file = await getChildFile(filename); + final content = await file.getContentAsString(); + return content!; + } + + void writeScopedFile(String filename, String content) async { + final file = await getChildFile(filename); + file.writeToFileAsString(content: content); + } + + Future scopedExists(String filename) async { + final scopedStorageFile = await getChildFile(filename); + final exists = await scopedStorageFile.exists(); + return exists!; + } + + void deleteScoped(String filename) async { + final file = await getChildFile(filename); + file.delete(); + } + + Future getChildFile(String filename) async { + final file = await findFile(uri!, filename); + if (file != null) { + return file; + } + DocumentFile? createdFile = + await createFile(uri!, mimeType: 'text/plain', displayName: filename); + return createdFile!; + } +} + Future getPath() async { WidgetsFlutterBinding.ensureInitialized(); final preferences = await SharedPreferences.getInstance(); @@ -19,16 +63,16 @@ Future getPath() async { } return SavePath.normal(path); } else { - SavePath path = await defaultPath; - preferences.setString('save_path', path.path!); + String path = await defaultPath; + preferences.setString('save_path', path); preferences.setBool('is_android_scoped', false); - return path; + return SavePath.normal(path); } } -Future get defaultPath async { +Future get defaultPath async { final directory = await _directory; - return SavePath.normal(directory.path); + return directory.path; } Future get _directory async { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 72aaa98..6fc30c1 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -345,10 +345,9 @@ class SavePathSetting extends StatefulWidget implements SettingTile { } Future resetSavePath() async { - savePath = await defaultPath; + savePath = SavePath.normal(await defaultPath); final preferences = await SharedPreferences.getInstance(); - SavePath path = await defaultPath; - preferences.setString('save_path', path.path!); + preferences.setString('save_path', await defaultPath); preferences.setBool('is_android_scoped', false); } diff --git a/lib/settings_notifier.dart b/lib/settings_notifier.dart index 63bdb21..a75d50d 100644 --- a/lib/settings_notifier.dart +++ b/lib/settings_notifier.dart @@ -1,6 +1,6 @@ -import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; +import 'package:daily_diary/path.dart'; import 'package:daily_diary/storage.dart'; class Settings { diff --git a/lib/storage.dart b/lib/storage.dart index c468552..fec0598 100644 --- a/lib/storage.dart +++ b/lib/storage.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:io'; -import 'package:daily_diary/main.dart'; import 'package:flutter/material.dart'; +import 'package:daily_diary/path.dart'; + import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:shared_storage/saf.dart'; import 'package:toml/toml.dart'; diff --git a/test/storage/diary_storage_test.dart b/test/storage/diary_storage_test.dart index 3ac13d6..5cd0387 100644 --- a/test/storage/diary_storage_test.dart +++ b/test/storage/diary_storage_test.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:daily_diary/path.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:daily_diary/main.dart'; import 'package:daily_diary/storage.dart'; main() { diff --git a/test/storage/settings_storage_test.dart b/test/storage/settings_storage_test.dart index 741d17f..6a8bbd1 100644 --- a/test/storage/settings_storage_test.dart +++ b/test/storage/settings_storage_test.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:daily_diary/main.dart'; +import 'package:daily_diary/path.dart'; import 'package:daily_diary/storage.dart'; main() { diff --git a/test/widget_test.dart b/test/widget_test.dart index 89acc89..9c1e0ac 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:daily_diary/main.dart'; +import 'package:daily_diary/path.dart'; import 'package:daily_diary/screens/settings.dart'; main() { From c96303724d8141afb4a127c0ed0d8d0f8f0a7c12 Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Mon, 22 May 2023 01:02:38 +0100 Subject: [PATCH 6/9] Refactor `savePath` reset and `SharedPreferences` --- lib/main.dart | 5 +++-- lib/path.dart | 27 +++++++++++++++++++------- lib/screens/settings.dart | 40 +++++++++++++++++---------------------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a03edb7..462eee8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,7 @@ import 'package:daily_diary/storage.dart'; import 'package:daily_diary/themes.dart'; import 'package:daily_diary/screens/home.dart'; -import 'package:shared_storage/saf.dart'; +import 'package:shared_preferences/shared_preferences.dart'; // This will be removed when widgets can react to spell check changes bool? startupCheckSpelling; @@ -31,7 +31,8 @@ main() async { class App extends StatelessWidget { const App({Key? key}) : super(key: key); - static final SettingsNotifier settingsNotifier = SettingsNotifier(savePath!); + static final settingsNotifier = SettingsNotifier(savePath!); + static final preferences = SharedPreferences.getInstance(); @override Widget build(BuildContext context) { diff --git a/lib/path.dart b/lib/path.dart index 799651d..14b89ad 100644 --- a/lib/path.dart +++ b/lib/path.dart @@ -2,8 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:daily_diary/main.dart'; + import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_storage/saf.dart'; class SavePath { @@ -53,7 +54,7 @@ class SavePath { Future getPath() async { WidgetsFlutterBinding.ensureInitialized(); - final preferences = await SharedPreferences.getInstance(); + final preferences = await App.preferences; String? path = preferences.getString('save_path'); bool? isAndroidScoped = preferences.getBool('is_android_scoped'); if (path != null) { @@ -63,14 +64,26 @@ Future getPath() async { } return SavePath.normal(path); } else { - String path = await defaultPath; - preferences.setString('save_path', path); - preferences.setBool('is_android_scoped', false); - return SavePath.normal(path); + return resetPathToDefault(); } } -Future get defaultPath async { +/// Resets the savePath to default in `SharedPreferences` and returns the +/// `SavePath` it was set to. It does NOT set the global `savePath`. +/// +/// To set `savePath` do: +/// ``` +/// savePath = await resetPathToDefault(); +/// ``` +Future resetPathToDefault() async { + final preferences = await App.preferences; + String path = await _defaultPath; + preferences.setString('save_path', path); + preferences.setBool('is_android_scoped', false); + return SavePath.normal(path); +} + +Future get _defaultPath async { final directory = await _directory; return directory.path; } diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 6fc30c1..0133425 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -2,14 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:daily_diary/main.dart'; +import 'package:daily_diary/path.dart'; + import 'package:file_picker/file_picker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_storage/shared_storage.dart' as saf; -import 'package:daily_diary/main.dart'; -import 'package:daily_diary/path.dart'; - class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @@ -340,17 +339,10 @@ class SavePathSetting extends StatefulWidget implements SettingTile { @override Future newDefault() async { - await resetSavePath(); + savePath = await resetPathToDefault(); return const SavePathSetting(); } - Future resetSavePath() async { - savePath = SavePath.normal(await defaultPath); - final preferences = await SharedPreferences.getInstance(); - preferences.setString('save_path', await defaultPath); - preferences.setBool('is_android_scoped', false); - } - @override State createState() => _SavePathSettingState(); } @@ -372,27 +364,19 @@ class _SavePathSettingState extends State { if (Platform.isAndroid) { return _askForPathAndroid(); } - // Load SharedPreferences while user is picking a path - final preferencesFuture = SharedPreferences.getInstance(); String? path = await FilePicker.platform.getDirectoryPath(); if (path == null) { return null; } - final preferences = await preferencesFuture; + final preferences = await App.preferences; preferences.setString('save_path', path); preferences.setBool('is_android_scoped', false); return SavePath.normal(path); } Future _askForPathAndroid() async { - // Load SharedPreferences while user is picking a path - final preferencesFuture = SharedPreferences.getInstance(); - // Remove previous permissions - if (savePath != null && savePath!.uri != null) { - final previousPath = savePath!.uri!; - saf.releasePersistableUriPermission(previousPath); - } + _removePreviousPermissions(); // Ask user for path and permissions Uri? uri; @@ -404,12 +388,22 @@ class _SavePathSettingState extends State { final asDocumentFile = await uri.toDocumentFile(); Map asMap = asDocumentFile!.toMap(); String asString = json.encode(asMap); - final preferences = await preferencesFuture; + final preferences = await App.preferences; preferences.setString('save_path', asString); preferences.setBool('is_android_scoped', true); return SavePath.android(uri); } + Future _removePreviousPermissions() async { + SavePath? path = savePath; + if (path == null) return; + + Uri? previousPath = path.uri; + if (previousPath == null) return; + + await saf.releasePersistableUriPermission(previousPath); + } + @override Widget build(BuildContext context) { return Column( From 8ae0016e253d38f10150ca091061be71db44952d Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Mon, 22 May 2023 16:37:44 +0100 Subject: [PATCH 7/9] Improve save location setting --- lib/path.dart | 9 ++++++++- lib/screens/settings.dart | 10 ++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/path.dart b/lib/path.dart index 14b89ad..fa040ef 100644 --- a/lib/path.dart +++ b/lib/path.dart @@ -17,7 +17,14 @@ class SavePath { bool get isScopedStorage => path == null; - String get string => isScopedStorage ? uri!.toString() : path!; + String get string { + if (isScopedStorage) { + String fullString = Uri.decodeFull(uri!.path); + return fullString.split(':').last; + } else { + return path!.replaceFirst('/storage/emulated/0/', ''); + } + } Future getScopedFile(String filename) async { final file = await getChildFile(filename); diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 0133425..29b41f0 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -409,12 +409,10 @@ class _SavePathSettingState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility( - visible: Platform.isAndroid, - child: Text( - 'Changing this setting will only work properly if the device is rooted:', - style: TextStyle(color: Theme.of(context).colorScheme.primary), - )), + Text( + 'Save Location:', + style: Theme.of(context).textTheme.titleMedium, + ), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: TextField( From 5124c7ab5779bdf457e987d252156dfb563d46b8 Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Wed, 31 May 2023 13:02:38 +0100 Subject: [PATCH 8/9] Fix desktop previous entry view --- lib/storage.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/storage.dart b/lib/storage.dart index fec0598..36257ac 100644 --- a/lib/storage.dart +++ b/lib/storage.dart @@ -231,7 +231,7 @@ class PreviousEntryStorage { if (path.isScopedStorage) { return await _readFileAndroid(); } - final file = File('$path/$filename'); + final file = File('${path.path}/$filename'); final contents = await file.readAsString(); return contents; } catch (error) { From df5522b5cf95e4437102984e053a2734bef2a306 Mon Sep 17 00:00:00 2001 From: Alex Gorichev Date: Wed, 31 May 2023 13:45:08 +0100 Subject: [PATCH 9/9] Bump version and update changelog --- metadata/en-GB/changelogs/9.txt | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 metadata/en-GB/changelogs/9.txt diff --git a/metadata/en-GB/changelogs/9.txt b/metadata/en-GB/changelogs/9.txt new file mode 100644 index 0000000..441fa91 --- /dev/null +++ b/metadata/en-GB/changelogs/9.txt @@ -0,0 +1 @@ +Allow changing directory on Android without root \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 14848b3..a4c00d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: A diary that starts with a blank page every day (while saving the p # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.2.4+8 +version: 1.2.5+9 environment: sdk: '>=2.19.0-374.1.beta <3.0.0'