diff --git a/.flutter b/.flutter index 5dcb86f..a14f74f 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 5dcb86f68f239346676ceb1ed1ea385bd215fba1 +Subproject commit a14f74ff3a1cbd521163c5f03d68113d50af93d3 diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 49f7246..7383f66 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -111,6 +111,14 @@ class GitLab extends AppSource { } } + @override + Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, + Map additionalSettings) async { + String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {}); + String optionalAuth = (PAT != null) ? 'private_token=$PAT' : ''; + return '$apkUrl?$optionalAuth'; + } + @override Future getLatestAPKDetails( String standardUrl, diff --git a/lib/pages/app.dart b/lib/pages/app.dart index ada2054..4166480 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -226,18 +226,26 @@ class _AppPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 20), - app?.icon != null - ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - GestureDetector( - child: Image.memory( - app!.icon!, - height: 150, - gaplessPlayback: true, - ), - onTap: () => pm.openApp(app.app.id), - ) - ]) - : Container(), + FutureBuilder( + future: appsProvider.updateAppIcon(app?.app.id), + builder: (ctx, val) { + return app?.icon != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: app == null + ? null + : () => pm.openApp(app.app.id), + child: Image.memory( + app!.icon!, + height: 150, + gaplessPlayback: true, + ), + ) + ]) + : Container(); + }), const SizedBox( height: 25, ), diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index d55a393..0485a15 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -354,7 +354,11 @@ class AppsPageState extends State { SliverFillRemaining( child: Center( child: Text( - appsProvider.apps.isEmpty ? tr('noApps') : tr('noAppsForFilter'), + appsProvider.apps.isEmpty + ? appsProvider.loadingApps + ? tr('pleaseWait') + : tr('noApps') + : tr('noAppsForFilter'), style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center, ))), @@ -402,29 +406,36 @@ class AppsPageState extends State { } getAppIcon(int appIndex) { - return listedApps[appIndex].icon != null - ? Image.memory( - listedApps[appIndex].icon!, - gaplessPlayback: true, - ) - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationZ(0.31), - child: Padding( - padding: const EdgeInsets.all(15), - child: Image( - image: const AssetImage( - 'assets/graphics/icon_small.png'), - color: Colors.white.withOpacity(0.3), - colorBlendMode: BlendMode.modulate, - gaplessPlayback: true, - ), - )), - ]); + return FutureBuilder( + future: appsProvider.updateAppIcon(listedApps[appIndex].app.id), + builder: (ctx, val) { + return listedApps[appIndex].icon != null + ? Image.memory( + listedApps[appIndex].icon!, + gaplessPlayback: true, + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform( + alignment: Alignment.center, + transform: Matrix4.rotationZ(0.31), + child: Padding( + padding: const EdgeInsets.all(15), + child: Image( + image: const AssetImage( + 'assets/graphics/icon_small.png'), + color: Theme.of(context).brightness == + Brightness.dark + ? Colors.white.withOpacity(0.4) + : Colors.white.withOpacity(0.3), + colorBlendMode: BlendMode.modulate, + gaplessPlayback: true, + ), + )), + ]); + }); } getVersionText(int appIndex) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 7df6eca..d37f494 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -5,6 +5,7 @@ import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form.dart'; +import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/main.dart'; import 'package:obtainium/providers/apps_provider.dart'; @@ -945,6 +946,25 @@ class _LogsDialogState extends State { ], ), actions: [ + TextButton( + onPressed: () async { + var cont = (await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('appLogs'), + items: const [], + initValid: true, + message: tr('removeFromObtainium'), + ); + })) != + null; + if (cont) { + logsProvider.clear(); + Navigator.of(context).pop(); + } + }, + child: Text(tr('remove'))), TextButton( onPressed: () { Navigator.of(context).pop(); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 6542f7c..e1f349c 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -329,6 +329,10 @@ Future> getHeaders(String url, return returnHeaders; } +Future> getAllInstalledInfo() async { + return await pm.getInstalledPackages() ?? []; +} + Future getInstalledInfo(String? packageName, {bool printErr = true}) async { if (packageName != null) { @@ -364,7 +368,9 @@ class AppsProvider with ChangeNotifier { foregroundStream = FGBGEvents.stream.asBroadcastStream(); foregroundSubscription = foregroundStream?.listen((event) async { isForeground = event == FGBGType.foreground; - if (isForeground) loadApps(); + if (isForeground) { + await loadApps(); + } }); () async { await settingsProvider.initializeSettings(); @@ -1160,17 +1166,6 @@ class AppsProvider with ChangeNotifier { : false; } - Future updateInstallStatusInMemory(AppInMemory app) async { - apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id); - apps[app.app.id]?.icon = - await apps[app.app.id]?.installedInfo?.applicationInfo?.getAppIcon(); - apps[app.app.id]?.app.name = await (apps[app.app.id] - ?.installedInfo - ?.applicationInfo - ?.getAppLabel()) ?? - app.name; - } - Future loadApps({String? singleId}) async { while (loadingApps) { await Future.delayed(const Duration(microseconds: 1)); @@ -1179,6 +1174,8 @@ class AppsProvider with ChangeNotifier { notifyListeners(); var sp = SourceProvider(); List> errors = []; + var installedAppsData = await getAllInstalledInfo(); + List removedAppIds = []; await Future.wait((await getAppsDir()) // Parse Apps from JSON .listSync() .map((item) async { @@ -1199,43 +1196,53 @@ class AppsProvider with ChangeNotifier { } } if (app != null) { + // Save the app to the in-memory list without grabbing any OS info first + apps.update( + app.id, + (value) => AppInMemory( + app!, value.downloadProgress, value.installedInfo, value.icon), + ifAbsent: () => AppInMemory(app!, null, null, null)); + notifyListeners(); try { + // Try getting the app's source to ensure no invalid apps get loaded sp.getSource(app.url, overrideSource: app.overrideSource); + // If the app is installed, grab its OS data and reconcile install statuses + PackageInfo? installedInfo; + try { + installedInfo = + installedAppsData.firstWhere((i) => i.packageName == app!.id); + } catch (e) { + // If the app isn't installed the above throws an error + } + // Reconcile differences between the installed and recorded install info + var moddedApp = + getCorrectedInstallStatusAppIfPossible(app, installedInfo); + if (moddedApp != null) { + app = moddedApp; + // Note the app ID if it was uninstalled externally + if (moddedApp.installedVersion == null) { + removedAppIds.add(moddedApp.id); + } + } + // Update the app in memory with install info and corrections apps.update( app.id, - (value) => AppInMemory(app!, value.downloadProgress, - value.installedInfo, value.icon), - ifAbsent: () => AppInMemory(app!, null, null, null)); + (value) => AppInMemory( + app!, value.downloadProgress, installedInfo, value.icon), + ifAbsent: () => AppInMemory(app!, null, installedInfo, null)); + notifyListeners(); } catch (e) { - errors.add([app.id, app.finalName, e.toString()]); + errors.add([app!.id, app.finalName, e.toString()]); } } })); - notifyListeners(); if (errors.isNotEmpty) { removeApps(errors.map((e) => e[0]).toList()); NotificationsProvider().notify( AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); } - // Get install status and other OS info for each App (slow) - List modifiedApps = []; - await Future.wait(apps.values.map((app) async { - await updateInstallStatusInMemory(app); - var moddedApp = - getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo); - if (moddedApp != null) { - modifiedApps.add(moddedApp); - } - })); - notifyListeners(); - // Reconcile version differences - if (modifiedApps.isNotEmpty) { - await saveApps(modifiedApps, attemptToCorrectInstallStatus: false); - var removedAppIds = modifiedApps - .where((a) => a.installedVersion == null) - .map((e) => e.id) - .toList(); - // After reconciliation, delete externally uninstalled Apps if needed + // Delete externally uninstalled Apps if needed + if (removedAppIds.isNotEmpty) { if (removedAppIds.isNotEmpty) { if (settingsProvider.removeOnExternalUninstall) { await removeApps(removedAppIds); @@ -1246,6 +1253,22 @@ class AppsProvider with ChangeNotifier { notifyListeners(); } + Future updateAppIcon(String? appId) async { + if (apps[appId]?.icon == null) { + var icon = + (await apps[appId]?.installedInfo?.applicationInfo?.getAppIcon()); + if (icon != null) { + apps.update( + apps[appId]!.app.id, + (value) => AppInMemory(apps[appId]!.app, value.downloadProgress, + value.installedInfo, icon), + ifAbsent: () => AppInMemory( + apps[appId]!.app, null, apps[appId]?.installedInfo, icon)); + notifyListeners(); + } + } + } + Future saveApps(List apps, {bool attemptToCorrectInstallStatus = true, bool onlyIfExists = true}) async { @@ -1941,8 +1964,7 @@ Future bgUpdateCheck(String taskId, Map? params) async { await appsProvider.downloadAndInstallLatestApps( toInstall.map((e) => e.key).toList(), null, notificationsProvider: notificationsProvider, - forceParallelDownloads: true, - useExisting: false); + forceParallelDownloads: true); } catch (e) { if (e is MultiAppMultiError) { e.idsByErrorString.forEach((key, value) { diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 20de673..73d9b46 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -354,11 +354,13 @@ preStandardizeUrl(String url) { url.toLowerCase().indexOf('https://') != 0) { url = 'https://$url'; } + var trailingSlash = Uri.tryParse(url)?.path.endsWith('/') ?? false; url = url - .split('/') - .where((e) => e.isNotEmpty) - .join('/') - .replaceFirst(':/', '://'); + .split('/') + .where((e) => e.isNotEmpty) + .join('/') + .replaceFirst(':/', '://') + + (trailingSlash ? '/' : ''); return url; } @@ -523,8 +525,7 @@ abstract class AppSource { [GeneratedFormTextField('appName', label: tr('appName'), required: false)], [ GeneratedFormSwitch('shizukuPretendToBeGooglePlay', - label: tr('shizukuPretendToBeGooglePlay'), - defaultValue: false) + label: tr('shizukuPretendToBeGooglePlay'), defaultValue: false) ], [ GeneratedFormSwitch('exemptFromBackgroundUpdates', diff --git a/pubspec.lock b/pubspec.lock index fa8d129..37b4d9d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -47,18 +47,18 @@ packages: dependency: "direct main" description: name: app_links - sha256: "8c6ef5ba9e26b720d4c9073826befb87df2ab5e7a81c22b6c3145080b5e736c9" + sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.1" archive: dependency: transitive description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.0" args: dependency: transitive description: @@ -443,10 +443,10 @@ packages: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" intl: dependency: transitive description: @@ -889,10 +889,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_ios: dependency: transitive description: @@ -977,18 +977,18 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: dad3313c9ead95517bb1cae5e1c9d20ba83729d5a59e5e83c0a2d66203f27f91 + sha256: "2282ba2320af34b2bd5320156c664d73f3f022341ed78847bc87723bf88c142f" url: "https://pub.dev" source: hosted - version: "3.16.1" + version: "3.16.2" webview_flutter_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b648824..a477eaf 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: 1.1.9+2266 +version: 1.1.10+2267 environment: sdk: '>=3.0.0 <4.0.0'