diff --git a/assets/translations/br.json b/assets/translations/br.json index 6a70a80..96e9e7d 100644 --- a/assets/translations/br.json +++ b/assets/translations/br.json @@ -254,6 +254,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Remover App?", "other": "Remover Apps?" diff --git a/assets/translations/bs.json b/assets/translations/bs.json index 254f11c..01d0d2b 100644 --- a/assets/translations/bs.json +++ b/assets/translations/bs.json @@ -111,7 +111,7 @@ "dark": "Tamna", "light": "Svijetla", "followSystem": "Pratite sistem", - "obtainium": "Obtainium", + "obtainium": "Obtainium", "materialYou": "Material You", "useBlackTheme": "Koristite čisto crnu tamnu temu", "appSortBy": "Aplikacije sortirane po", @@ -251,7 +251,9 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", - "removeAppQuestion": { + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", + "removeAppQuestion": { "one": "Želite li ukloniti aplikaciju?", "other": "Želite li ukloniti aplikacije?" }, diff --git a/assets/translations/de.json b/assets/translations/de.json index 81c2ea7..f6f9d0e 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "App entfernen?", "other": "Apps entfernen?" diff --git a/assets/translations/en.json b/assets/translations/en.json index f286c68..4395c68 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -254,6 +254,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/es.json b/assets/translations/es.json index 8893461..b662041 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "¿Eliminar Aplicación?", "other": "¿Eliminar Aplicaciones?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index adf44c2..c3f5274 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "برنامه حذف شود؟", "other": "برنامه ها حذف شوند؟" diff --git a/assets/translations/fr.json b/assets/translations/fr.json index cfae07f..8576909 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Supprimer l'application ?", "other": "Supprimer les applications ?" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 1913544..845e313 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -250,6 +250,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Eltávolítja az alkalmazást?", "other": "Eltávolítja az alkalmazást?" diff --git a/assets/translations/it.json b/assets/translations/it.json index b211e4e..7fec3c8 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Rimuovere l'app?", "other": "Rimuovere le app?" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 1181f88..541da17 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -252,6 +252,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "アプリを削除しますか?", "other": "アプリを削除しますか?" diff --git a/assets/translations/pl.json b/assets/translations/pl.json index 1b43982..04f8322 100644 --- a/assets/translations/pl.json +++ b/assets/translations/pl.json @@ -257,6 +257,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Usunąć aplikację?", "few": "Usunąć aplikacje?", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 49bc22d..690cd53 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -251,6 +251,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "Удалить приложение?", "other": "Удалить приложения?" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index ac43598..66fc426 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -252,6 +252,8 @@ "versionExtractionRegEx": "Version Extraction RegEx", "matchGroupToUse": "Match Group to Use", "highlightTouchTargets": "Highlight less obvious touch targets", + "pickExportDir": "Pick Export Directory", + "autoExportOnChanges": "Auto-export on changes", "removeAppQuestion": { "one": "是否删除应用?", "other": "是否删除应用?" diff --git a/build.sh b/build.sh index 0ad2412..76fcdde 100755 --- a/build.sh +++ b/build.sh @@ -4,7 +4,9 @@ CURR_DIR="$(pwd)" trap "cd "$CURR_DIR"" EXIT -git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date +if [ -z "$1" ]; then + git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date +fi rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs) for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index a2a25cc..8676af9 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -124,10 +124,7 @@ class HTML extends AppSource { additionalValidators: [ (value) { value ??= '1'; - if (int.tryParse(value) == null) { - return tr('invalidInput'); - } - return null; + return intValidator(value); } ]) ] diff --git a/lib/main.dart b/lib/main.dart index 1b9fe28..0b2338d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.14.12'; +const String currentVersion = '0.14.13'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 45812d0..dcd52bd 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -338,9 +338,9 @@ class _AppPageState extends State { try { HapticFeedback.heavyImpact(); var res = await appsProvider.downloadAndInstallLatestApps( - app?.app.id != null ? [app!.app.id] : [], - globalNavigatorKey.currentContext, - settingsProvider); + app?.app.id != null ? [app!.app.id] : [], + globalNavigatorKey.currentContext, + ); if (app?.app.installedVersion != null && !trackOnly) { // ignore: use_build_context_synchronously showError(tr('appsUpdated'), context); diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index a980724..3f4cc30 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -381,8 +381,7 @@ class AppsPageState extends State { : () { appsProvider.downloadAndInstallLatestApps( [listedApps[appIndex].app.id], - globalNavigatorKey.currentContext, - settingsProvider).catchError((e) { + globalNavigatorKey.currentContext).catchError((e) { showError(e, context); return []; }); @@ -459,7 +458,9 @@ class AppsPageState extends State { : Theme.of(context).primaryColorLight) .withAlpha(20) : null), - padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + padding: settingsProvider.highlightTouchTargets + ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0) + : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, @@ -697,8 +698,8 @@ class AppsPageState extends State { toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); } appsProvider - .downloadAndInstallLatestApps(toInstall, - globalNavigatorKey.currentContext, settingsProvider) + .downloadAndInstallLatestApps( + toInstall, globalNavigatorKey.currentContext) .catchError((e) { showError(e, context); return []; diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index e2ebdf9..159d38a 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -28,8 +28,8 @@ class _ImportExportPageState extends State { @override Widget build(BuildContext context) { SourceProvider sourceProvider = SourceProvider(); - var appsProvider = context.read(); - var settingsProvider = context.read(); + var appsProvider = context.watch(); + var settingsProvider = context.watch(); var outlineButtonStyle = ButtonStyle( shape: MaterialStateProperty.all( @@ -102,10 +102,16 @@ class _ImportExportPageState extends State { }); } - runObtainiumExport() { + runObtainiumExport() async { HapticFeedback.selectionClick(); - appsProvider.exportApps().then((String path) { - showError(tr('exportedTo', args: [path]), context); + appsProvider + .exportApps( + pickOnly: (await settingsProvider.getExportDir()) == null, + sp: settingsProvider) + .then((String? result) { + if (result != null) { + showError(tr('exportedTo', args: [result]), context); + } }).catchError((e) { showError(e, context); }); @@ -301,27 +307,68 @@ class _ImportExportPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - Expanded( - child: TextButton( - style: outlineButtonStyle, - onPressed: appsProvider.apps.isEmpty || - importInProgress - ? null - : runObtainiumExport, - child: Text(tr('obtainiumExport')))), - const SizedBox( - width: 16, - ), - Expanded( - child: TextButton( - style: outlineButtonStyle, - onPressed: importInProgress - ? null - : runObtainiumImport, - child: Text(tr('obtainiumImport')))) - ], + FutureBuilder( + future: settingsProvider.getExportDir(), + builder: (context, snapshot) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: appsProvider.apps.isEmpty || + importInProgress + ? null + : runObtainiumExport, + child: Text(tr(snapshot.data != null + ? 'obtainiumExport' + : 'pickExportDir')), + )), + const SizedBox( + width: 16, + ), + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: importInProgress + ? null + : runObtainiumImport, + child: Text(tr('obtainiumImport')))) + ], + ), + if (snapshot.data != null) + Column( + children: [ + const SizedBox(height: 16), + GeneratedForm( + items: [ + [ + GeneratedFormSwitch( + 'autoExportOnChanges', + label: tr('autoExportOnChanges'), + defaultValue: settingsProvider + .autoExportOnChanges, + ) + ] + ], + onValueChanges: + (value, valid, isBuilding) { + if (valid && !isBuilding) { + if (value['autoExportOnChanges'] != + null) { + settingsProvider + .autoExportOnChanges = value[ + 'autoExportOnChanges'] == + true; + } + } + }), + ], + ), + ], + ); + }, ), if (importInProgress) const Column( @@ -399,7 +446,7 @@ class _ImportExportPageState extends State { fontStyle: FontStyle.italic, fontSize: 12)), const SizedBox( height: 8, - ) + ), ], ))) ])); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 208b222..715a558 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -31,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart'; import 'package:http/http.dart'; import 'package:android_intent_plus/android_intent.dart'; import 'package:flutter_archive/flutter_archive.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; final pm = AndroidPackageManager(); @@ -150,6 +151,7 @@ class AppsProvider with ChangeNotifier { late Stream? foregroundStream; late StreamSubscription? foregroundSubscription; late Directory APKDir; + late SettingsProvider settingsProvider = SettingsProvider(); Iterable getAppValues() => apps.values.map((a) => a.deepCopy()); @@ -161,6 +163,7 @@ class AppsProvider with ChangeNotifier { if (isForeground) await loadApps(); }); () async { + await settingsProvider.initializeSettings(); var cacheDirs = await getExternalCacheDirectories(); if (cacheDirs?.isNotEmpty ?? false) { APKDir = cacheDirs!.first; @@ -369,8 +372,7 @@ class AppsProvider with ChangeNotifier { .where((element) => element.downloadProgress != null) .isNotEmpty; - Future canInstallSilently( - App app, SettingsProvider settingsProvider) async { + Future canInstallSilently(App app) async { if (app.id == obtainiumId) { return false; } @@ -539,7 +541,6 @@ class AppsProvider with ChangeNotifier { getHost(apkUrl.value) != getHost(app.url) && context != null) { // ignore: use_build_context_synchronously - var settingsProvider = context.read(); if (!(settingsProvider.hideAPKOriginWarning) && // ignore: use_build_context_synchronously await showDialog( @@ -560,8 +561,8 @@ class AppsProvider with ChangeNotifier { // If no BuildContext is provided, apps that require user interaction are ignored // If user input is needed and the App is in the background, a notification is sent to get the user's attention // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result - Future> downloadAndInstallLatestApps(List appIds, - BuildContext? context, SettingsProvider settingsProvider, + Future> downloadAndInstallLatestApps( + List appIds, BuildContext? context, {NotificationsProvider? notificationsProvider}) async { notificationsProvider = notificationsProvider ?? context?.read(); @@ -590,8 +591,7 @@ class AppsProvider with ChangeNotifier { apps[id]!.app.preferredApkIndex = urlInd; await saveApps([apps[id]!.app]); } - if (context != null || - await canInstallSilently(apps[id]!.app, settingsProvider)) { + if (context != null || await canInstallSilently(apps[id]!.app)) { appsToInstall.add(id); } } @@ -628,8 +628,7 @@ class AppsProvider with ChangeNotifier { downloadedDir = downloadedArtifact as DownloadedXApkDir; } var appId = downloadedFile?.appId ?? downloadedDir!.appId; - bool willBeSilent = - await canInstallSilently(apps[appId]!.app, settingsProvider); + bool willBeSilent = await canInstallSilently(apps[appId]!.app); if (!(await settingsProvider.getInstallPermission(enforce: false))) { throw ObtainiumError(tr('cancelled')); } @@ -678,8 +677,8 @@ class AppsProvider with ChangeNotifier { } Future getAppsDir() async { - Directory appsDir = Directory( - '${(await getExternalStorageDirectory())?.path as String}/app_data'); + Directory appsDir = + Directory('${(await getExternalStorageDirectory())!.path}/app_data'); if (!appsDir.existsSync()) { appsDir.createSync(); } @@ -879,8 +878,6 @@ class AppsProvider with ChangeNotifier { .toList(); // After reconciliation, delete externally uninstalled Apps if needed if (removedAppIds.isNotEmpty) { - var settingsProvider = SettingsProvider(); - await settingsProvider.initializeSettings(); if (settingsProvider.removeOnExternalUninstall) { await removeApps(removedAppIds); } @@ -919,6 +916,7 @@ class AppsProvider with ChangeNotifier { } } notifyListeners(); + await exportApps(isAuto: true); } Future removeApps(List appIds) async { @@ -940,6 +938,7 @@ class AppsProvider with ChangeNotifier { } if (appIds.isNotEmpty) { notifyListeners(); + await exportApps(isAuto: true); } } @@ -1096,32 +1095,48 @@ class AppsProvider with ChangeNotifier { return updateAppIds; } - Future exportApps() async { - if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { - if (await Permission.storage.isDenied) { - await Permission.storage.request(); + Future exportApps( + {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async { + SettingsProvider settingsProvider = sp ?? this.settingsProvider; + var exportDir = await settingsProvider.getExportDir(); + if (isAuto) { + if (exportDir == null) { + logs.add('Skipping auto-export as dir is not set.'); + return null; } - if (await Permission.storage.isDenied) { - throw ObtainiumError(tr('storagePermissionDenied')); + logs.add('Started auto-export.'); + var files = await saf + .listFiles(exportDir, columns: [saf.DocumentFileColumn.id]) + .where((f) => f.uri.pathSegments.last.endsWith('-auto.json')) + .toList(); + if (files.isNotEmpty) { + for (var f in files) { + saf.delete(f.uri); + } + logs.add('Previous auto-export deleted.'); } } - Directory? exportDir = Directory('/storage/emulated/0/Download'); - String path = 'Downloads'; // TODO: See if hardcoding this can be avoided - var downloadsAccessible = false; - try { - downloadsAccessible = exportDir.existsSync(); - } catch (e) { - logs.add('Error accessing Downloads (will use fallback): $e'); + if (exportDir == null || pickOnly) { + await settingsProvider.pickExportDir(); + exportDir = await settingsProvider.getExportDir(); } - if (!downloadsAccessible) { - exportDir = await getExternalStorageDirectory(); - path = exportDir!.path; + if (exportDir == null) { + return null; } - File export = File( - '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); - export.writeAsStringSync( - jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); - return path; + String? returnPath; + if (!pickOnly) { + var result = await saf.createFile(exportDir, + displayName: + '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', + mimeType: 'application/json', + content: jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); + if (result == null) { + throw ObtainiumError(tr('unexpectedError')); + } + returnPath = + exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/'); + } + return returnPath; } Future importApps(String appsJSON) async { @@ -1298,14 +1313,12 @@ Future bgUpdateCheck(int taskId, Map? params) async { NotificationsProvider notificationsProvider = NotificationsProvider(); AppsProvider appsProvider = AppsProvider(isBg: true); await appsProvider.loadApps(); - var settingsProvider = SettingsProvider(); - await settingsProvider.initializeSettings(); int maxAttempts = 4; params ??= {}; if (params['toCheck'] == null) { - settingsProvider.lastBGCheckTime = DateTime.now(); + appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); } List> toCheck = >[ ...(params['toCheck'] @@ -1335,7 +1348,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { var didCompleteChecking = false; CheckingUpdatesNotification? notif; var networkRestricted = false; - if (settingsProvider.bgUpdatesOnWiFiOnly) { + if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { var netResult = await (Connectivity().checkConnectivity()); networkRestricted = (netResult != ConnectivityResult.wifi) && (netResult != ConnectivityResult.ethernet); @@ -1355,8 +1368,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { App? newApp = await appsProvider.checkUpdate(appId); if (newApp != null) { if (networkRestricted || - !(await appsProvider.canInstallSilently( - app!.app, settingsProvider))) { + !(await appsProvider.canInstallSilently(app!.app))) { toNotify.add(newApp); } else { toInstall.add(MapEntry(appId, 0)); @@ -1442,8 +1454,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { try { logs.add( 'BG install task $taskId: Attempting to update $appId in the background.'); - await appsProvider.downloadAndInstallLatestApps( - [appId], null, settingsProvider, + await appsProvider.downloadAndInstallLatestApps([appId], null, notificationsProvider: notificationsProvider); await Future.delayed(const Duration( seconds: diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a95e2b3..2085168 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/main.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; String obtainiumId = 'dev.imranr.obtainium'; @@ -35,6 +37,7 @@ List updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] class SettingsProvider with ChangeNotifier { SharedPreferences? prefs; + String? defaultAppDir; bool justStarted = true; String sourceUrl = 'https://github.com/ImranR98/Obtainium'; @@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier { // Not done in constructor as we want to be able to await it Future initializeSettings() async { prefs = await SharedPreferences.getInstance(); + defaultAppDir = (await getExternalStorageDirectory())!.path; notifyListeners(); } @@ -357,4 +361,49 @@ class SettingsProvider with ChangeNotifier { prefs?.setBool('highlightTouchTargets', val); notifyListeners(); } + + Future getExportDir() async { + var uriString = prefs?.getString('exportDir'); + if (uriString != null) { + Uri? uri = Uri.parse(uriString); + if (!(await saf.canRead(uri) ?? false) || + !(await saf.canWrite(uri) ?? false)) { + uri = null; + prefs?.remove('exportDir'); + notifyListeners(); + } + return uri; + } else { + return null; + } + } + + Future pickExportDir({bool remove = false}) async { + var existingSAFPerms = (await saf.persistedUriPermissions()) ?? []; + var currentOneWayDataSyncDir = await getExportDir(); + Uri? newOneWayDataSyncDir; + if (!remove) { + newOneWayDataSyncDir = (await saf.openDocumentTree()); + } + if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) { + if (newOneWayDataSyncDir == null) { + prefs?.remove('exportDir'); + } else { + prefs?.setString('exportDir', newOneWayDataSyncDir.toString()); + } + notifyListeners(); + } + for (var e in existingSAFPerms) { + await saf.releasePersistableUriPermission(e.uri); + } + } + + bool get autoExportOnChanges { + return prefs?.getBool('autoExportOnChanges') ?? false; + } + + set autoExportOnChanges(bool val) { + prefs?.setBool('autoExportOnChanges', val); + notifyListeners(); + } } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 3c06be2..43a0f1e 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -521,6 +521,20 @@ regExValidator(String? value) { return null; } +intValidator(String? value, {bool positive = false}) { + if (value == null) { + return tr('invalidInput'); + } + var num = int.tryParse(value); + if (num == null) { + return tr('invalidInput'); + } + if (positive && num <= 0) { + return tr('invalidInput'); + } + return null; +} + class SourceProvider { // Add more source classes here so they are available via the service List get sources => [ diff --git a/pubspec.lock b/pubspec.lock index 04ca07a..cdacfb5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -686,6 +686,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + shared_storage: + dependency: "direct main" + description: + name: shared_storage + sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6" + url: "https://pub.dev" + source: hosted + version: "0.8.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 4f48012..09d66b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.14.12+204 # When changing this, update the tag in main() accordingly +version: 0.14.13+205 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0' @@ -65,6 +65,7 @@ dependencies: flutter_archive: ^5.0.0 hsluv: ^1.1.3 connectivity_plus: ^4.0.2 + shared_storage: ^0.8.0 dev_dependencies: flutter_test: