diff --git a/lib/components/generated_form_modal.dart b/lib/components/generated_form_modal.dart index 7a6ed44..27df229 100644 --- a/lib/components/generated_form_modal.dart +++ b/lib/components/generated_form_modal.dart @@ -7,11 +7,15 @@ class GeneratedFormModal extends StatefulWidget { {super.key, required this.title, required this.items, - required this.defaultValues}); + required this.defaultValues, + this.initValid = false, + this.message = ""}); final String title; + final String message; final List> items; final List defaultValues; + final bool initValid; @override State createState() => _GeneratedFormModalState(); @@ -21,20 +25,34 @@ class _GeneratedFormModalState extends State { List values = []; bool valid = false; + @override + void initState() { + super.initState(); + valid = widget.initValid; + } + @override Widget build(BuildContext context) { return AlertDialog( scrollable: true, title: Text(widget.title), - content: GeneratedForm( - items: widget.items, - onValueChanges: (values, valid) { - setState(() { - this.values = values; - this.valid = valid; - }); - }, - defaultValues: widget.defaultValues), + content: + Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (widget.message.isNotEmpty) Text(widget.message), + if (widget.message.isNotEmpty) + SizedBox( + height: 16, + ), + GeneratedForm( + items: widget.items, + onValueChanges: (values, valid) { + setState(() { + this.values = values; + this.valid = valid; + }); + }, + defaultValues: widget.defaultValues) + ]), actions: [ TextButton( onPressed: () { diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 96236cf..ec24c9a 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -247,9 +247,8 @@ class _AppPageState extends State { onPressed: () { HapticFeedback .selectionClick(); - appsProvider - .removeApp(app!.app.id) - .then((_) { + appsProvider.removeApps( + [app!.app.id]).then((_) { int count = 0; Navigator.of(context) .popUntil((_) => diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 84be9b9..1744241 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -43,7 +43,6 @@ class AppsPageState extends State { Widget build(BuildContext context) { var appsProvider = context.watch(); var settingsProvider = context.watch(); - var existingUpdateAppIds = appsProvider.getExistingUpdates(); var sortedApps = appsProvider.apps.values.toList(); selectedIds = selectedIds @@ -63,7 +62,11 @@ class AppsPageState extends State { if (filter != null) { sortedApps = sortedApps.where((app) { if (app.app.installedVersion == app.app.latestVersion && - filter!.onlyNonLatest) { + !(filter!.includeUptodate)) { + return false; + } + if (app.app.installedVersion == null && + !(filter!.includeNonInstalled)) { return false; } if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) { @@ -184,38 +187,133 @@ class AppsPageState extends State { ? 'Select All' : 'Deselect ${selectedIds.length.toString()}')), const VerticalDivider(), - const Spacer(), - selectedIds.isEmpty - ? const SizedBox() - : IconButton( - onPressed: () { - // TODO: Delete selected Apps after confirming - }, - icon: const Icon(Icons.install_mobile_outlined)), - selectedIds.isEmpty - ? const SizedBox() - : IconButton( - onPressed: () { - // TODO: Install selected Apps if they are not up to date after confirming (replace existing button) - }, - icon: const Icon(Icons.delete_outline_rounded)), - existingUpdateAppIds.isEmpty || filter != null - ? const SizedBox() - : IconButton( - onPressed: appsProvider.areDownloadsRunning() - ? null - : () { - HapticFeedback.heavyImpact(); - settingsProvider.getInstallPermission().then((_) { - appsProvider.downloadAndInstallLatestApp( - existingUpdateAppIds, context); - }); - }, - icon: const Icon(Icons.install_mobile_outlined), - ), + Expanded( + child: selectedIds.isEmpty + ? Container() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () { + showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: 'Remove Selected Apps?', + items: const [], + defaultValues: const [], + initValid: true, + message: + '${selectedIds.length} App${selectedIds.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedIds.length == 1 ? 'it' : 'them'} manually.', + ); + }).then((values) { + if (values != null) { + appsProvider.removeApps(selectedIds.toList()); + } + }); + }, + icon: const Icon(Icons.delete_outline_outlined), + ), + IconButton( + onPressed: appsProvider.areDownloadsRunning() || + selectedIds + .where((id) => + appsProvider.apps[id]!.app + .installedVersion != + appsProvider + .apps[id]!.app.latestVersion) + .isEmpty + ? null + : () { + HapticFeedback.heavyImpact(); + var existingUpdateIdsSelected = + appsProvider + .getExistingUpdates( + installedOnly: true) + .where((element) => + selectedIds.contains(element)) + .toList(); + var newInstallIdsSelected = appsProvider + .getExistingUpdates( + nonInstalledOnly: true) + .where((element) => + selectedIds.contains(element)) + .toList(); + List> formInputs = + []; + if (existingUpdateIdsSelected + .isNotEmpty && + newInstallIdsSelected.isNotEmpty) { + formInputs.add([ + GeneratedFormItem( + label: + "Update ${existingUpdateIdsSelected.length} Apps?", + type: FormItemType.bool) + ]); + formInputs.add([ + GeneratedFormItem( + label: + "Install ${newInstallIdsSelected.length} new Apps?", + type: FormItemType.bool) + ]); + } + showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: "Install Selected Apps?", + message: + "${existingUpdateIdsSelected.length} update${existingUpdateIdsSelected.length == 1 ? '' : 's'} and ${newInstallIdsSelected.length} new install${newInstallIdsSelected.length == 1 ? '' : 's'}.", + items: formInputs, + defaultValues: const [ + "true", + "true" + ], + initValid: true, + ); + }).then((values) { + if (values != null) { + bool shouldInstallUpdates = + values.length < 2 || + values[0] == "true"; + bool shouldInstallNew = + values.length < 2 || + values[1] == "true"; + settingsProvider + .getInstallPermission() + .then((_) { + List toInstall = []; + if (shouldInstallUpdates) { + toInstall.addAll( + existingUpdateIdsSelected); + } + if (shouldInstallNew) { + toInstall.addAll( + newInstallIdsSelected); + } + appsProvider + .downloadAndInstallLatestApp( + toInstall, context); + }); + } + }); + }, + icon: const Icon( + Icons.file_download_outlined, + )), + ], + )), + const VerticalDivider(), appsProvider.apps.isEmpty ? const SizedBox() - : IconButton( + : TextButton.icon( + label: Text( + filter == null ? 'Filter' : 'Filter *', + style: TextStyle( + fontWeight: filter == null + ? FontWeight.normal + : FontWeight.bold), + ), onPressed: () { showDialog?>( context: context, @@ -231,27 +329,25 @@ class AppsPageState extends State { ], [ GeneratedFormItem( - label: "Ignore Up-to-Date Apps", + label: "Up to Date Apps", + type: FormItemType.bool) + ], + [ + GeneratedFormItem( + label: "Non-Installed Apps", type: FormItemType.bool) ] ], defaultValues: filter == null - ? [] - : [ - filter!.nameFilter, - filter!.authorFilter, - filter!.onlyNonLatest ? 'true' : '' - ]); + ? AppsFilter().toValuesArray() + : filter!.toValuesArray()); }).then((values) { - if (values != null && - values - .where((element) => element.isNotEmpty) - .isNotEmpty) { + if (values != null) { setState(() { - filter = AppsFilter( - nameFilter: values[0], - authorFilter: values[1], - onlyNonLatest: values[2] == "true"); + filter = AppsFilter.fromValuesArray(values); + if (AppsFilter().isIdenticalTo(filter!)) { + filter = null; + } }); } else { setState(() { @@ -260,8 +356,7 @@ class AppsPageState extends State { } }); }, - icon: Icon( - filter == null ? Icons.search : Icons.manage_search)) + icon: const Icon(Icons.filter_list_rounded)) ], ), ], @@ -272,10 +367,34 @@ class AppsPageState extends State { class AppsFilter { late String nameFilter; late String authorFilter; - late bool onlyNonLatest; + late bool includeUptodate; + late bool includeNonInstalled; AppsFilter( {this.nameFilter = "", this.authorFilter = "", - this.onlyNonLatest = false}); + this.includeUptodate = true, + this.includeNonInstalled = true}); + + List toValuesArray() { + return [ + nameFilter, + authorFilter, + includeUptodate ? "true" : "", + includeNonInstalled ? "true" : "" + ]; + } + + AppsFilter.fromValuesArray(List values) { + nameFilter = values[0]; + authorFilter = values[1]; + includeUptodate = values[2] == "true"; + includeNonInstalled = values[3] == "true"; + } + + bool isIdenticalTo(AppsFilter other) => + authorFilter.trim() == other.authorFilter.trim() && + nameFilter.trim() == other.nameFilter.trim() && + includeUptodate == other.includeUptodate && + includeNonInstalled == other.includeNonInstalled; } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 3bb4317..7c6dbfc 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -98,7 +98,7 @@ class AppsProvider with ChangeNotifier { .isNotEmpty; Future canInstallSilently(App app) async { - // TODO: This is unreliable - try to get from OS + // TODO: This is unreliable - try to get from OS in the future var osInfo = await DeviceInfoPlugin().androidInfo; return app.installedVersion != null && osInfo.version.sdkInt! >= 30 && @@ -203,9 +203,11 @@ class AppsProvider with ChangeNotifier { } if (context != null) { - for (var i in regularInstalls) { + if (regularInstalls.isNotEmpty) { // ignore: use_build_context_synchronously await askUserToReturnToForeground(context); + } + for (var i in regularInstalls) { await installApk(i); } } @@ -256,15 +258,19 @@ class AppsProvider with ChangeNotifier { notifyListeners(); } - Future removeApp(String appId) async { - File file = File('${(await getAppsDir()).path}/$appId.json'); - if (file.existsSync()) { - file.deleteSync(); + Future removeApps(List appIds) async { + for (var appId in appIds) { + File file = File('${(await getAppsDir()).path}/$appId.json'); + if (file.existsSync()) { + file.deleteSync(); + } + if (apps.containsKey(appId)) { + apps.remove(appId); + } } - if (apps.containsKey(appId)) { - apps.remove(appId); + if (appIds.isNotEmpty) { + notifyListeners(); } - notifyListeners(); } bool checkAppObjectForUpdate(App app) { @@ -309,14 +315,20 @@ class AppsProvider with ChangeNotifier { return updates; } - List getExistingUpdates({bool installedOnly = false}) { + List getExistingUpdates( + {bool installedOnly = false, bool nonInstalledOnly = false}) { List updateAppIds = []; List appIds = apps.keys.toList(); for (int i = 0; i < appIds.length; i++) { App? app = apps[appIds[i]]!.app; if (app.installedVersion != app.latestVersion && - (app.installedVersion != null || !installedOnly)) { - updateAppIds.add(app.id); + (!installedOnly || !nonInstalledOnly)) { + if ((app.installedVersion == null && + (nonInstalledOnly || !installedOnly) || + (app.installedVersion != null && + (installedOnly || !nonInstalledOnly)))) { + updateAppIds.add(app.id); + } } } return updateAppIds;