From 45e5544c5be575babc055bb3288c8a2d042e1774 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 24 Sep 2022 21:10:29 -0400 Subject: [PATCH 1/6] Added apps list selection (actions incomplete) --- lib/main.dart | 2 +- lib/pages/apps.dart | 304 +++++++++++++++++++++++++++----------------- lib/pages/home.dart | 8 +- 3 files changed, 195 insertions(+), 119 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 17864a7..95e3da4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -99,7 +99,7 @@ class MyApp extends StatelessWidget { if (settingsProvider.updateInterval > 0) { Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check', frequency: Duration(minutes: settingsProvider.updateInterval), - // initialDelay: Duration(minutes: settingsProvider.updateInterval), + initialDelay: Duration(minutes: settingsProvider.updateInterval), constraints: Constraints(networkType: NetworkType.connected), existingWorkPolicy: ExistingWorkPolicy.replace); } else { diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 86c37ab..84be9b9 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -12,11 +12,32 @@ class AppsPage extends StatefulWidget { const AppsPage({super.key}); @override - State createState() => _AppsPageState(); + State createState() => AppsPageState(); } -class _AppsPageState extends State { +class AppsPageState extends State { AppsFilter? filter; + Set selectedIds = {}; + + clearSelected() { + if (selectedIds.isNotEmpty) { + setState(() { + selectedIds.clear(); + }); + return true; + } + return false; + } + + selectThese(List appIds) { + if (selectedIds.isEmpty) { + setState(() { + for (var a in appIds) { + selectedIds.add(a); + } + }); + } + } @override Widget build(BuildContext context) { @@ -25,6 +46,20 @@ class _AppsPageState extends State { var existingUpdateAppIds = appsProvider.getExistingUpdates(); var sortedApps = appsProvider.apps.values.toList(); + selectedIds = selectedIds + .where((element) => sortedApps.map((e) => e.app.id).contains(element)) + .toSet(); + + toggleAppSelected(String appId) { + setState(() { + if (selectedIds.contains(appId)) { + selectedIds.remove(appId); + } else { + selectedIds.add(appId); + } + }); + } + if (filter != null) { sortedApps = sortedApps.where((app) { if (app.app.installedVersion == app.app.latestVersion && @@ -74,126 +109,163 @@ class _AppsPageState extends State { } return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - floatingActionButton: - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - existingUpdateAppIds.isEmpty || filter != null - ? const SizedBox() - : ElevatedButton.icon( - onPressed: appsProvider.areDownloadsRunning() - ? null - : () { - HapticFeedback.heavyImpact(); - settingsProvider.getInstallPermission().then((_) { - appsProvider.downloadAndInstallLatestApp( - existingUpdateAppIds, context); - }); - }, - icon: const Icon(Icons.install_mobile_outlined), - label: const Text('Install All')), - const SizedBox( - width: 16, - ), - appsProvider.apps.isEmpty - ? const SizedBox() - : ElevatedButton.icon( - onPressed: () { - showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: 'Filter Apps', - items: [ - [ - GeneratedFormItem( - label: "App Name", required: false), - GeneratedFormItem( - label: "Author", required: false) - ], - [ - GeneratedFormItem( - label: "Ignore Up-to-Date Apps", - type: FormItemType.bool) - ] - ], - defaultValues: filter == null - ? [] - : [ - filter!.nameFilter, - filter!.authorFilter, - filter!.onlyNonLatest ? 'true' : '' - ]); - }).then((values) { - if (values != null && - values - .where((element) => element.isNotEmpty) - .isNotEmpty) { - setState(() { - filter = AppsFilter( - nameFilter: values[0], - authorFilter: values[1], - onlyNonLatest: values[2] == "true"); - }); - } else { - setState(() { - filter = null; - }); - } - }); - }, - label: Text(filter == null ? 'Search' : 'Modify Search'), - icon: Icon( - filter == null ? Icons.search : Icons.manage_search)), - ]), - body: RefreshIndicator( - onRefresh: () { - HapticFeedback.lightImpact(); - return appsProvider.checkUpdates().catchError((e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - }); - }, - child: CustomScrollView(slivers: [ - const CustomAppBar(title: 'Apps'), - if (appsProvider.loadingApps || sortedApps.isEmpty) - SliverFillRemaining( - child: Center( - child: appsProvider.loadingApps - ? const CircularProgressIndicator() - : Text( - appsProvider.apps.isEmpty - ? 'No Apps' - : 'No Search Results', - style: - Theme.of(context).textTheme.headlineMedium, - ))), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return ListTile( - title: Text(sortedApps[index].app.name), - subtitle: Text('By ${sortedApps[index].app.author}'), - trailing: sortedApps[index].downloadProgress != null - ? Text( - 'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%') - : (sortedApps[index].app.installedVersion != null && - sortedApps[index].app.installedVersion != - sortedApps[index].app.latestVersion - ? const Text('Update Available') - : Text(sortedApps[index].app.installedVersion ?? - 'Not Installed')), - onTap: () { + backgroundColor: Theme.of(context).colorScheme.surface, + body: RefreshIndicator( + onRefresh: () { + HapticFeedback.lightImpact(); + return appsProvider.checkUpdates().catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + }); + }, + child: CustomScrollView(slivers: [ + const CustomAppBar(title: 'Apps'), + if (appsProvider.loadingApps || sortedApps.isEmpty) + SliverFillRemaining( + child: Center( + child: appsProvider.loadingApps + ? const CircularProgressIndicator() + : Text( + appsProvider.apps.isEmpty + ? 'No Apps' + : 'No Search Results', + style: Theme.of(context).textTheme.headlineMedium, + ))), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return ListTile( + selectedTileColor: + Theme.of(context).colorScheme.primary.withOpacity(0.1), + selected: selectedIds.contains(sortedApps[index].app.id), + onLongPress: () { + toggleAppSelected(sortedApps[index].app.id); + }, + title: Text(sortedApps[index].app.name), + subtitle: Text('By ${sortedApps[index].app.author}'), + trailing: sortedApps[index].downloadProgress != null + ? Text( + 'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%') + : (sortedApps[index].app.installedVersion != null && + sortedApps[index].app.installedVersion != + sortedApps[index].app.latestVersion + ? const Text('Update Available') + : Text(sortedApps[index].app.installedVersion ?? + 'Not Installed')), + onTap: () { + if (selectedIds.isNotEmpty) { + toggleAppSelected(sortedApps[index].app.id); + } else { Navigator.push( context, MaterialPageRoute( builder: (context) => AppPage(appId: sortedApps[index].app.id)), ); - }, - ); - }, childCount: sortedApps.length)) - ]))); + } + }, + ); + }, childCount: sortedApps.length)) + ])), + persistentFooterButtons: [ + Row( + children: [ + TextButton.icon( + onPressed: () { + selectedIds.isEmpty + ? selectThese(sortedApps.map((e) => e.app.id).toList()) + : clearSelected(); + }, + icon: Icon(selectedIds.isEmpty + ? Icons.select_all_outlined + : Icons.deselect_outlined), + label: Text(selectedIds.isEmpty + ? '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), + ), + appsProvider.apps.isEmpty + ? const SizedBox() + : IconButton( + onPressed: () { + showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: 'Filter Apps', + items: [ + [ + GeneratedFormItem( + label: "App Name", required: false), + GeneratedFormItem( + label: "Author", required: false) + ], + [ + GeneratedFormItem( + label: "Ignore Up-to-Date Apps", + type: FormItemType.bool) + ] + ], + defaultValues: filter == null + ? [] + : [ + filter!.nameFilter, + filter!.authorFilter, + filter!.onlyNonLatest ? 'true' : '' + ]); + }).then((values) { + if (values != null && + values + .where((element) => element.isNotEmpty) + .isNotEmpty) { + setState(() { + filter = AppsFilter( + nameFilter: values[0], + authorFilter: values[1], + onlyNonLatest: values[2] == "true"); + }); + } else { + setState(() { + filter = null; + }); + } + }); + }, + icon: Icon( + filter == null ? Icons.search : Icons.manage_search)) + ], + ), + ], + ); } } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 96d5683..c1718de 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -25,7 +25,8 @@ class _HomePageState extends State { List selectedIndexHistory = []; List pages = [ - NavigationPageItem('Apps', Icons.apps, const AppsPage()), + NavigationPageItem( + 'Apps', Icons.apps, AppsPage(key: GlobalKey())), NavigationPageItem('Add App', Icons.add, const AddAppPage()), NavigationPageItem( 'Import/Export', Icons.import_export, const ImportExportPage()), @@ -88,7 +89,10 @@ class _HomePageState extends State { }); return false; } - return true; + return !(pages[0].widget.key as GlobalKey) + .currentState + ?.clearSelected(); + // return !appsPageKey.currentState?.clearSelected(); }); } } From f58d26524c631ea41f76a7be1d24e875ae74e8d7 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 25 Sep 2022 00:12:02 -0400 Subject: [PATCH 2/6] Done w/ filter and multi select stuff --- lib/components/generated_form_modal.dart | 38 +++- lib/pages/app.dart | 5 +- lib/pages/apps.dart | 221 +++++++++++++++++------ lib/providers/apps_provider.dart | 36 ++-- 4 files changed, 224 insertions(+), 76 deletions(-) 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; From 9a4b0301be3124b54d61ccea84b056eda0f0edbf Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 25 Sep 2022 00:21:41 -0400 Subject: [PATCH 3/6] Updated version, standardized quotes, deleted test_page --- lib/app_sources/github.dart | 14 +++---- lib/components/generated_form.dart | 10 ++--- lib/components/generated_form_modal.dart | 4 +- lib/main.dart | 4 +- lib/pages/add_app.dart | 10 ++--- lib/pages/apps.dart | 36 ++++++++-------- lib/pages/test_page.dart | 53 ------------------------ lib/providers/apps_provider.dart | 3 -- lib/providers/source_provider.dart | 2 +- pubspec.yaml | 2 +- 10 files changed, 41 insertions(+), 97 deletions(-) delete mode 100644 lib/pages/test_page.dart diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index c957a37..6cbfc3f 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -21,9 +21,9 @@ class GitHub implements AppSource { Future getLatestAPKDetails( String standardUrl, List additionalData) async { var includePrereleases = - additionalData.isNotEmpty && additionalData[0] == "true"; + additionalData.isNotEmpty && additionalData[0] == 'true'; var fallbackToOlderReleases = - additionalData.length >= 2 && additionalData[1] == "true"; + additionalData.length >= 2 && additionalData[1] == 'true'; var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty ? additionalData[2] : null; @@ -92,14 +92,14 @@ class GitHub implements AppSource { @override List> additionalDataFormItems = [ - [GeneratedFormItem(label: "Include prereleases", type: FormItemType.bool)], + [GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)], [ GeneratedFormItem( - label: "Fallback to older releases", type: FormItemType.bool) + label: 'Fallback to older releases', type: FormItemType.bool) ], [ GeneratedFormItem( - label: "Filter Release Titles by Regular Expression", + label: 'Filter Release Titles by Regular Expression', type: FormItemType.string, required: false, additionalValidators: [ @@ -110,7 +110,7 @@ class GitHub implements AppSource { try { RegExp(value); } catch (e) { - return "Invalid regular expression"; + return 'Invalid regular expression'; } return null; } @@ -119,5 +119,5 @@ class GitHub implements AppSource { ]; @override - List additionalDataDefaults = ["true", "true", ""]; + List additionalDataDefaults = ['true', 'true', '']; } diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index 24577f7..d646b0f 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -12,7 +12,7 @@ class GeneratedFormItem { late List additionalValidators; GeneratedFormItem( - {this.label = "Input", + {this.label = 'Input', this.type = FormItemType.string, this.required = true, this.max = 1, @@ -69,7 +69,7 @@ class _GeneratedFormState extends State { .map((row) => row.map((e) { return j < widget.defaultValues.length ? widget.defaultValues[j++] - : ""; + : ''; }).toList()) .toList(); @@ -89,7 +89,7 @@ class _GeneratedFormState extends State { }); }, decoration: InputDecoration( - helperText: e.value.label + (e.value.required ? " *" : "")), + helperText: e.value.label + (e.value.required ? ' *' : '')), minLines: e.value.max <= 1 ? null : e.value.max, maxLines: e.value.max <= 1 ? 1 : e.value.max, validator: (value) { @@ -122,10 +122,10 @@ class _GeneratedFormState extends State { children: [ Text(widget.items[r][e].label), Switch( - value: values[r][e] == "true", + value: values[r][e] == 'true', onChanged: (value) { setState(() { - values[r][e] = value ? "true" : ""; + values[r][e] = value ? 'true' : ''; someValueChanged(); }); }) diff --git a/lib/components/generated_form_modal.dart b/lib/components/generated_form_modal.dart index 27df229..ab6e558 100644 --- a/lib/components/generated_form_modal.dart +++ b/lib/components/generated_form_modal.dart @@ -9,7 +9,7 @@ class GeneratedFormModal extends StatefulWidget { required this.items, required this.defaultValues, this.initValid = false, - this.message = ""}); + this.message = ''}); final String title; final String message; @@ -40,7 +40,7 @@ class _GeneratedFormModalState extends State { Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.message.isNotEmpty) Text(widget.message), if (widget.message.isNotEmpty) - SizedBox( + const SizedBox( height: 16, ), GeneratedForm( diff --git a/lib/main.dart b/lib/main.dart index 95e3da4..9d9db25 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; const String currentReleaseTag = - 'v0.3.2-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES + 'v0.4.0-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES @pragma('vm:entry-point') void bgTaskCallback() { @@ -118,7 +118,7 @@ class MyApp extends StatelessWidget { currentReleaseTag, [], 0, - ["true"])); + ['true'])); } } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index e8a909c..a86b0cc 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -19,7 +19,7 @@ class AddAppPage extends StatefulWidget { class _AddAppPageState extends State { bool gettingAppInfo = false; - String userInput = ""; + String userInput = ''; AppSource? pickedSource; List additionalData = []; bool validAdditionalData = true; @@ -44,19 +44,19 @@ class _AddAppPageState extends State { items: [ [ GeneratedFormItem( - label: "App Source Url", + label: 'App Source Url', additionalValidators: [ (value) { try { sourceProvider - .getSource(value ?? "") + .getSource(value ?? '') .standardizeURL( makeUrlHttps( - value ?? "")); + value ?? '')); } catch (e) { return e is String ? e - : "Error"; + : 'Error'; } return null; } diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 1744241..aad265f 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -247,13 +247,13 @@ class AppsPageState extends State { formInputs.add([ GeneratedFormItem( label: - "Update ${existingUpdateIdsSelected.length} Apps?", + 'Update ${existingUpdateIdsSelected.length} Apps?', type: FormItemType.bool) ]); formInputs.add([ GeneratedFormItem( label: - "Install ${newInstallIdsSelected.length} new Apps?", + 'Install ${newInstallIdsSelected.length} new Apps?', type: FormItemType.bool) ]); } @@ -261,13 +261,13 @@ class AppsPageState extends State { context: context, builder: (BuildContext ctx) { return GeneratedFormModal( - title: "Install Selected Apps?", + title: 'Install Selected Apps?', message: - "${existingUpdateIdsSelected.length} update${existingUpdateIdsSelected.length == 1 ? '' : 's'} and ${newInstallIdsSelected.length} new install${newInstallIdsSelected.length == 1 ? '' : 's'}.", + '${existingUpdateIdsSelected.length} update${existingUpdateIdsSelected.length == 1 ? '' : 's'} and ${newInstallIdsSelected.length} new install${newInstallIdsSelected.length == 1 ? '' : 's'}.', items: formInputs, defaultValues: const [ - "true", - "true" + 'true', + 'true' ], initValid: true, ); @@ -275,10 +275,10 @@ class AppsPageState extends State { if (values != null) { bool shouldInstallUpdates = values.length < 2 || - values[0] == "true"; + values[0] == 'true'; bool shouldInstallNew = values.length < 2 || - values[1] == "true"; + values[1] == 'true'; settingsProvider .getInstallPermission() .then((_) { @@ -323,18 +323,18 @@ class AppsPageState extends State { items: [ [ GeneratedFormItem( - label: "App Name", required: false), + label: 'App Name', required: false), GeneratedFormItem( - label: "Author", required: false) + label: 'Author', required: false) ], [ GeneratedFormItem( - label: "Up to Date Apps", + label: 'Up to Date Apps', type: FormItemType.bool) ], [ GeneratedFormItem( - label: "Non-Installed Apps", + label: 'Non-Installed Apps', type: FormItemType.bool) ] ], @@ -371,8 +371,8 @@ class AppsFilter { late bool includeNonInstalled; AppsFilter( - {this.nameFilter = "", - this.authorFilter = "", + {this.nameFilter = '', + this.authorFilter = '', this.includeUptodate = true, this.includeNonInstalled = true}); @@ -380,16 +380,16 @@ class AppsFilter { return [ nameFilter, authorFilter, - includeUptodate ? "true" : "", - includeNonInstalled ? "true" : "" + includeUptodate ? 'true' : '', + includeNonInstalled ? 'true' : '' ]; } AppsFilter.fromValuesArray(List values) { nameFilter = values[0]; authorFilter = values[1]; - includeUptodate = values[2] == "true"; - includeNonInstalled = values[3] == "true"; + includeUptodate = values[2] == 'true'; + includeNonInstalled = values[3] == 'true'; } bool isIdenticalTo(AppsFilter other) => diff --git a/lib/pages/test_page.dart b/lib/pages/test_page.dart deleted file mode 100644 index c15679e..0000000 --- a/lib/pages/test_page.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:obtainium/components/generated_form.dart'; - -class TestPage extends StatefulWidget { - const TestPage({super.key}); - - @override - State createState() => _TestPageState(); -} - -class _TestPageState extends State { - List? sourceSpecificData; - bool valid = false; - - List> sourceSpecificInputs = [ - [GeneratedFormItem(label: 'Test Item 1')], - [ - GeneratedFormItem(label: 'Test Item 2', required: false), - GeneratedFormItem(label: 'Test Item 3') - ], - [GeneratedFormItem(label: 'Test Item 4', type: FormItemType.bool)] - ]; - - List defaultInputValues = ["ABC"]; - - void onSourceSpecificDataChanges( - List valuesFromForm, bool formValid) { - setState(() { - sourceSpecificData = valuesFromForm; - valid = formValid; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Test Page')), - backgroundColor: Theme.of(context).colorScheme.surface, - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column(children: [ - GeneratedForm( - items: sourceSpecificInputs, - onValueChanges: onSourceSpecificDataChanges, - defaultValues: defaultInputValues, - ), - ...(sourceSpecificData != null - ? (sourceSpecificData as List) - .map((e) => Text(e ?? "")) - : [Container()]) - ]))); - } -} diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 7c6dbfc..c92e3a7 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -113,9 +113,6 @@ class AppsProvider with ChangeNotifier { cancelExisting: true); await FGBGEvents.stream.first == FGBGType.foreground; await notificationsProvider.cancel(completeInstallationNotification.id); - // We need to wait for the App to come to the foreground to install it - // Can't try to call install plugin in a background isolate (may not have worked anyways) because of: - // https://github.com/flutter/flutter/issues/13937 } } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 1e3915f..095b139 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -85,7 +85,7 @@ class App { escapeRegEx(String s) { return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) { - return "\\${x[0]}"; + return '\\${x[0]}'; }); } diff --git a/pubspec.yaml b/pubspec.yaml index 057dad2..c803cd6 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.3.2+18 # When changing this, update the tag in main() accordingly +version: 0.4.0+19 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.19.0-79.0.dev <3.0.0' From 428c208de435cba7b937d8e6980e66dc5b178a35 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 25 Sep 2022 01:41:50 -0400 Subject: [PATCH 4/6] Added share option, saveApp -> saveApps --- lib/main.dart | 22 +++++++------- lib/pages/add_app.dart | 3 +- lib/pages/app.dart | 10 +++---- lib/pages/apps.dart | 16 +++++++++++ lib/pages/import_export.dart | 2 +- lib/providers/apps_provider.dart | 21 ++++++++------ pubspec.lock | 49 ++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 8 files changed, 98 insertions(+), 26 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9d9db25..d34806d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -109,16 +109,18 @@ class MyApp extends StatelessWidget { if (isFirstRun) { // If this is the first run, ask for notification permissions and add Obtainium to the Apps list Permission.notification.request(); - appsProvider.saveApp(App( - 'imranr98_obtainium_${GitHub().host}', - 'https://github.com/ImranR98/Obtainium', - 'ImranR98', - 'Obtainium', - currentReleaseTag, - currentReleaseTag, - [], - 0, - ['true'])); + appsProvider.saveApps([ + App( + 'imranr98_obtainium_${GitHub().host}', + 'https://github.com/ImranR98/Obtainium', + 'ImranR98', + 'Obtainium', + currentReleaseTag, + currentReleaseTag, + [], + 0, + ['true']) + ]); } } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index a86b0cc..43e8121 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -113,7 +113,8 @@ class _AddAppPageState extends State { settingsProvider .getInstallPermission() .then((_) { - appsProvider.saveApp(app).then((_) { + appsProvider + .saveApps([app]).then((_) { Navigator.push( context, MaterialPageRoute( diff --git a/lib/pages/app.dart b/lib/pages/app.dart index ec24c9a..736ee49 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -126,8 +126,8 @@ class _AppPageState extends State { .installedVersion = updatedApp .latestVersion; - appsProvider.saveApp( - updatedApp); + appsProvider.saveApps( + [updatedApp]); } Navigator.of(context) .pop(); @@ -167,8 +167,8 @@ class _AppPageState extends State { updatedApp .installedVersion = null; - appsProvider.saveApp( - updatedApp); + appsProvider.saveApps( + [updatedApp]); } Navigator.of(context) .pop(); @@ -202,7 +202,7 @@ class _AppPageState extends State { if (app != null && values != null) { var changedApp = app.app; changedApp.additionalData = values; - appsProvider.saveApp(changedApp); + appsProvider.saveApps([changedApp]); } }); }, diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index aad265f..808147c 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -7,6 +7,7 @@ import 'package:obtainium/pages/app.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -194,6 +195,7 @@ class AppsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( + visualDensity: VisualDensity.compact, onPressed: () { showDialog?>( context: context, @@ -215,6 +217,7 @@ class AppsPageState extends State { icon: const Icon(Icons.delete_outline_outlined), ), IconButton( + visualDensity: VisualDensity.compact, onPressed: appsProvider.areDownloadsRunning() || selectedIds .where((id) => @@ -301,6 +304,19 @@ class AppsPageState extends State { icon: const Icon( Icons.file_download_outlined, )), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + String urls = ''; + for (var id in selectedIds) { + urls += '${appsProvider.apps[id]!.app.url}\n'; + } + urls = urls.substring(0, urls.length - 1); + Share.share(urls, + subject: 'Selected App URLs from Obtainium'); + }, + icon: const Icon(Icons.share), + ), ], )), const VerticalDivider(), diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index d6c8435..994820a 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -47,7 +47,7 @@ class _ImportExportPageState extends State { if (appsProvider.apps.containsKey(app.id)) { errorsMap.addAll({app.id: 'App already added'}); } else { - await appsProvider.saveApp(app); + await appsProvider.saveApps([app]); } } List> errors = diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index c92e3a7..b5c3927 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -124,7 +124,7 @@ class AppsProvider with ChangeNotifier { await AppInstaller.installApk(file.file.path, actionRequired: false); apps[file.appId]!.app.installedVersion = apps[file.appId]!.app.latestVersion; - await saveApp(apps[file.appId]!.app); + await saveApps([apps[file.appId]!.app]); } // Given a list of AppIds, uses stored info about the apps to download APKs and install them @@ -171,7 +171,7 @@ class AppsProvider with ChangeNotifier { int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl); if (urlInd != apps[id]!.app.preferredApkIndex) { apps[id]!.app.preferredApkIndex = urlInd; - await saveApp(apps[id]!.app); + await saveApps([apps[id]!.app]); } if (context != null || (await canInstallSilently(apps[id]!.app) && @@ -247,11 +247,14 @@ class AppsProvider with ChangeNotifier { notifyListeners(); } - Future saveApp(App app) async { - File('${(await getAppsDir()).path}/${app.id}.json') - .writeAsStringSync(jsonEncode(app.toJson())); - apps.update(app.id, (value) => AppInMemory(app, value.downloadProgress), - ifAbsent: () => AppInMemory(app, null)); + Future saveApps(List apps) async { + for (var app in apps) { + File('${(await getAppsDir()).path}/${app.id}.json') + .writeAsStringSync(jsonEncode(app.toJson())); + this.apps.update( + app.id, (value) => AppInMemory(app, value.downloadProgress), + ifAbsent: () => AppInMemory(app, null)); + } notifyListeners(); } @@ -289,7 +292,7 @@ class AppsProvider with ChangeNotifier { if (currentApp.preferredApkIndex < newApp.apkUrls.length) { newApp.preferredApkIndex = currentApp.preferredApkIndex; } - await saveApp(newApp); + await saveApps([newApp]); return newApp; } return null; @@ -353,7 +356,7 @@ class AppsProvider with ChangeNotifier { for (App a in importedApps) { a.installedVersion = apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null; - await saveApp(a); + await saveApps([a]); } notifyListeners(); return importedApps.length; diff --git a/pubspec.lock b/pubspec.lock index 3219b7b..3992a85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -324,6 +324,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" nested: dependency: transitive description: @@ -457,6 +464,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.3" + share_plus: + dependency: "direct main" + description: + name: share_plus + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + share_plus_linux: + dependency: transitive + description: + name: share_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + share_plus_macos: + dependency: transitive + description: + name: share_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.3" + share_plus_web: + dependency: transitive + description: + name: share_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_windows: + dependency: transitive + description: + name: share_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c803cd6..afa428e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: file_picker: ^5.1.0 animations: ^2.0.4 flutter_install_app: ^1.3.0 + share_plus: ^4.4.0 dev_dependencies: From 33238b56a91f42c60e6efef06447552f82324062 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 25 Sep 2022 01:43:51 -0400 Subject: [PATCH 5/6] Added IconButton tootlips --- lib/pages/app.dart | 1 + lib/pages/apps.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 736ee49..2bcc573 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -206,6 +206,7 @@ class _AppPageState extends State { } }); }, + tooltip: 'Additional Options', icon: const Icon(Icons.settings)), const SizedBox(width: 16.0), Expanded( diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 808147c..3a539a9 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -214,6 +214,7 @@ class AppsPageState extends State { } }); }, + tooltip: 'Remove Selected Apps', icon: const Icon(Icons.delete_outline_outlined), ), IconButton( @@ -301,6 +302,7 @@ class AppsPageState extends State { } }); }, + tooltip: 'Install/Update Selected Apps', icon: const Icon( Icons.file_download_outlined, )), @@ -315,6 +317,7 @@ class AppsPageState extends State { Share.share(urls, subject: 'Selected App URLs from Obtainium'); }, + tooltip: 'Share Selected App URLs', icon: const Icon(Icons.share), ), ], From 33fed1cb2fcace239389874eda1ed34f2a0b15a1 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sun, 25 Sep 2022 01:56:24 -0400 Subject: [PATCH 6/6] Reduced dependece on fgbg thanks to new install plugin --- lib/providers/apps_provider.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b5c3927..252d8a4 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -105,14 +105,17 @@ class AppsProvider with ChangeNotifier { osInfo.version.release!.compareTo('12') >= 0; } - Future askUserToReturnToForeground(BuildContext context) async { + Future askUserToReturnToForeground(BuildContext context, + {bool waitForFG = false}) async { NotificationsProvider notificationsProvider = context.read(); if (!isForeground) { await notificationsProvider.notify(completeInstallationNotification, cancelExisting: true); - await FGBGEvents.stream.first == FGBGType.foreground; - await notificationsProvider.cancel(completeInstallationNotification.id); + if (waitForFG) { + await FGBGEvents.stream.first == FGBGType.foreground; + await notificationsProvider.cancel(completeInstallationNotification.id); + } } } @@ -143,8 +146,6 @@ class AppsProvider with ChangeNotifier { // If the App has more than one APK, the user should pick one (if context provided) String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex]; if (apps[id]!.app.apkUrls.length > 1 && context != null) { - // ignore: use_build_context_synchronously - await askUserToReturnToForeground(context); apkUrl = await showDialog( context: context, builder: (BuildContext ctx) { @@ -155,8 +156,6 @@ class AppsProvider with ChangeNotifier { if (apkUrl != null && Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin && context != null) { - // ignore: use_build_context_synchronously - await askUserToReturnToForeground(context); if (await showDialog( context: context, builder: (BuildContext ctx) {