diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 745542a..a0ff893 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -7,7 +7,6 @@ import 'package:obtainium/main.dart'; import 'package:obtainium/pages/apps.dart'; import 'package:obtainium/pages/settings.dart'; import 'package:obtainium/providers/apps_provider.dart'; -import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -165,42 +164,11 @@ class _AppPageState extends State { onTap: app?.app == null || updating ? null : () async { - var fileUrl = await appsProvider.confirmAppFileUrl( - app!.app, context, true); - if (fileUrl != null) { - NotificationsProvider notificationsProvider = - (globalNavigatorKey.currentContext ?? context) - .read(); - try { - showMessage( - '${tr('downloadingX', args: [fileUrl.key])}...', - globalNavigatorKey.currentContext ?? context); - await downloadFile( - fileUrl.value, - fileUrl.key - .split('.') - .reversed - .toList() - .sublist(1) - .reversed - .join('.'), (double? progress) { - notificationsProvider.notify(DownloadNotification( - fileUrl.key, progress?.ceil() ?? 0)); - }, '/storage/emulated/0/Download', - headers: await source?.getRequestHeaders( - app.app.additionalSettings, - forAPKDownload: fileUrl.key.endsWith('.apk') - ? true - : false)); - notificationsProvider.notify(DownloadedNotification( - fileUrl.key, fileUrl.value)); - } catch (e) { - showError( - e, globalNavigatorKey.currentContext ?? context); - } finally { - notificationsProvider - .cancel(DownloadNotification(fileUrl.key, 0).id); - } + try { + await appsProvider + .downloadAppAssets([app!.app.id], context); + } catch (e) { + showError(e, context); } }, child: Text( diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 52125ed..7da0e2b 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -854,69 +854,78 @@ class AppsPageState extends State { scrollable: true, content: Padding( padding: const EdgeInsets.only(top: 6), - child: Row( + child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - IconButton( + TextButton( + onPressed: pinSelectedApps, + child: Text(selectedApps + .where((element) => element.pinned) + .isEmpty + ? tr('pinToTop') + : tr('unpinFromTop'))), + const Divider(), + TextButton( + onPressed: () { + String urls = ''; + for (var a in selectedApps) { + urls += '${a.url}\n'; + } + urls = urls.substring(0, urls.length - 1); + Share.share(urls, + subject: 'Obtainium - ${tr('appsString')}'); + Navigator.of(context).pop(); + }, + child: Text(tr('shareSelectedAppURLs'))), + const Divider(), + TextButton( + onPressed: selectedAppIds.isEmpty + ? null + : () { + String urls = + '

${tr('customLinkMessage')}:

\n\n
    \n'; + for (var a in selectedApps) { + urls += + '
  • ${a.name}
  • \n'; + } + urls += + '
\n\n

${tr('about')}

'; + Share.share(urls, + subject: + 'Obtainium - ${tr('appsString')}'); + }, + child: Text(tr('shareAppConfigLinks'))), + const Divider(), + TextButton( + onPressed: () { + appsProvider + .downloadAppAssets( + selectedApps.map((e) => e.id).toList(), + globalNavigatorKey.currentContext ?? + context) + .catchError((e) => showError( + e, + globalNavigatorKey.currentContext ?? + context)); + Navigator.of(context).pop(); + }, + child: Text(tr('downloadX', + args: [tr('releaseAsset').toLowerCase()]))), + const Divider(), + TextButton( onPressed: appsProvider.areDownloadsRunning() ? null : showMassMarkDialog, - tooltip: tr('markSelectedAppsUpdated'), - icon: const Icon(Icons.done)), - IconButton( - onPressed: pinSelectedApps, - tooltip: selectedApps - .where((element) => element.pinned) - .isEmpty - ? tr('pinToTop') - : tr('unpinFromTop'), - icon: Icon(selectedApps - .where((element) => element.pinned) - .isEmpty - ? Icons.bookmark_outline_rounded - : Icons.bookmark_remove_outlined), - ), - IconButton( - onPressed: () { - String urls = ''; - for (var a in selectedApps) { - urls += '${a.url}\n'; - } - urls = urls.substring(0, urls.length - 1); - Share.share(urls, - subject: 'Obtainium - ${tr('appsString')}'); - Navigator.of(context).pop(); - }, - tooltip: tr('shareSelectedAppURLs'), - icon: const Icon(Icons.share_rounded), - ), - IconButton( - onPressed: selectedAppIds.isEmpty - ? null - : () { - String urls = - '

${tr('customLinkMessage')}:

\n\n
    \n'; - for (var a in selectedApps) { - urls += - '
  • ${a.name}
  • \n'; - } - urls += - '
\n\n

${tr('about')}

'; - Share.share(urls, - subject: 'Obtainium - ${tr('appsString')}'); - }, - tooltip: tr('shareAppConfigLinks'), - icon: const Icon(Icons.ios_share), - ), + child: Text(tr('markSelectedAppsUpdated'))), ]), ), ); diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index d926726..61c3379 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -916,6 +916,73 @@ class AppsProvider with ChangeNotifier { return installedIds; } + Future> downloadAppAssets( + List appIds, BuildContext context, + {bool forceParallelDownloads = false}) async { + NotificationsProvider notificationsProvider = + context.read(); + List, App>> filesToDownload = []; + for (var id in appIds) { + if (apps[id] == null) { + throw ObtainiumError(tr('appNotFound')); + } + MapEntry? fileUrl; + if (apps[id]!.app.apkUrls.isNotEmpty || + apps[id]!.app.otherAssetUrls.isNotEmpty) { + // ignore: use_build_context_synchronously + fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true); + } + if (fileUrl != null) { + filesToDownload.add(MapEntry(fileUrl, apps[id]!.app)); + } + } + + // Prepare to download+install Apps + MultiAppMultiError errors = MultiAppMultiError(); + List downloadedIds = []; + + Future downloadFn(MapEntry fileUrl, App app) async { + try { + await downloadFile( + fileUrl.value, + fileUrl.key + .split('.') + .reversed + .toList() + .sublist(1) + .reversed + .join('.'), (double? progress) { + notificationsProvider + .notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0)); + }, '/storage/emulated/0/Download', + headers: await SourceProvider() + .getSource(app.url, overrideSource: app.overrideSource) + .getRequestHeaders(app.additionalSettings, + forAPKDownload: + fileUrl.key.endsWith('.apk') ? true : false)); + notificationsProvider + .notify(DownloadedNotification(fileUrl.key, fileUrl.value)); + } catch (e) { + errors.add(fileUrl.key, e); + } finally { + notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id); + } + } + + if (forceParallelDownloads || !settingsProvider.parallelDownloads) { + for (var urlWithApp in filesToDownload) { + await downloadFn(urlWithApp.key, urlWithApp.value); + } + } else { + await Future.wait(filesToDownload + .map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value))); + } + if (errors.idsByErrorString.isNotEmpty) { + throw errors; + } + return downloadedIds; + } + Future getAppsDir() async { Directory appsDir = Directory('${(await getExternalStorageDirectory())!.path}/app_data');