From 9bd7ddb21b56c9c1b9aa85d80de85ce7e6eeae1b Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 12 Nov 2022 02:14:45 -0500 Subject: [PATCH] Added App pinning --- lib/app_sources/fdroid.dart | 1 - lib/main.dart | 3 +- lib/pages/apps.dart | 144 ++++++++++++++++++++--------- lib/providers/apps_provider.dart | 3 +- lib/providers/source_provider.dart | 17 ++-- 5 files changed, 113 insertions(+), 55 deletions(-) diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 25ad2fc..ea3d37c 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -1,6 +1,5 @@ import 'package:html/parser.dart'; import 'package:http/http.dart'; -import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; diff --git a/lib/main.dart b/lib/main.dart index 152ab9d..f69cf1a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -143,7 +143,8 @@ class _ObtainiumState extends State { [], 0, ['true'], - null) + null, + false) ]); } // Register the background update task according to the user's setting diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 437435e..f4c59af 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -23,24 +23,24 @@ class AppsPageState extends State { AppsFilter? filter; var updatesOnlyFilter = AppsFilter(includeUptodate: false, includeNonInstalled: false); - Set selectedIds = {}; + Set selectedApps = {}; DateTime? refreshingSince; clearSelected() { - if (selectedIds.isNotEmpty) { + if (selectedApps.isNotEmpty) { setState(() { - selectedIds.clear(); + selectedApps.clear(); }); return true; } return false; } - selectThese(List appIds) { - if (selectedIds.isEmpty) { + selectThese(List apps) { + if (selectedApps.isEmpty) { setState(() { - for (var a in appIds) { - selectedIds.add(a); + for (var a in apps) { + selectedApps.add(a); } }); } @@ -54,16 +54,16 @@ class AppsPageState extends State { var currentFilterIsUpdatesOnly = filter?.isIdenticalTo(updatesOnlyFilter) ?? false; - selectedIds = selectedIds - .where((element) => sortedApps.map((e) => e.app.id).contains(element)) + selectedApps = selectedApps + .where((element) => sortedApps.map((e) => e.app).contains(element)) .toSet(); - toggleAppSelected(String appId) { + toggleAppSelected(App app) { setState(() { - if (selectedIds.contains(appId)) { - selectedIds.remove(appId); + if (selectedApps.contains(app)) { + selectedApps.remove(app); } else { - selectedIds.add(appId); + selectedApps.add(app); } }); } @@ -124,15 +124,15 @@ class AppsPageState extends State { var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true); var existingUpdateIdsAllOrSelected = existingUpdates - .where((element) => selectedIds.isEmpty + .where((element) => selectedApps.isEmpty ? sortedApps.where((a) => a.app.id == element).isNotEmpty - : selectedIds.contains(element)) + : selectedApps.map((e) => e.id).contains(element)) .toList(); var newInstallIdsAllOrSelected = appsProvider .findExistingUpdates(nonInstalledOnly: true) - .where((element) => selectedIds.isEmpty + .where((element) => selectedApps.isEmpty ? sortedApps.where((a) => a.app.id == element).isNotEmpty - : selectedIds.contains(element)) + : selectedApps.map((e) => e.id).contains(element)) .toList(); if (settingsProvider.pinUpdates) { @@ -147,6 +147,17 @@ class AppsPageState extends State { sortedApps = [...temp, ...sortedApps]; } + var tempPinned = []; + var tempNotPinned = []; + for (var a in sortedApps) { + if (a.app.pinned) { + tempPinned.add(a); + } else { + tempNotPinned.add(a); + } + } + sortedApps = [...tempPinned, ...tempNotPinned]; + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: RefreshIndicator( @@ -192,11 +203,16 @@ class AppsPageState extends State { delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return ListTile( - selectedTileColor: - Theme.of(context).colorScheme.primary.withOpacity(0.1), - selected: selectedIds.contains(sortedApps[index].app.id), + tileColor: sortedApps[index].app.pinned + ? Colors.grey.withOpacity(0.1) + : Colors.transparent, + selectedTileColor: Theme.of(context) + .colorScheme + .primary + .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1), + selected: selectedApps.contains(sortedApps[index].app), onLongPress: () { - toggleAppSelected(sortedApps[index].app.id); + toggleAppSelected(sortedApps[index].app); }, leading: sortedApps[index].installedInfo != null ? Image.memory( @@ -204,9 +220,19 @@ class AppsPageState extends State { gaplessPlayback: true, ) : null, - title: Text(sortedApps[index].installedInfo?.name ?? - sortedApps[index].app.name), - subtitle: Text('By ${sortedApps[index].app.author}'), + title: Text( + sortedApps[index].installedInfo?.name ?? + sortedApps[index].app.name, + style: TextStyle( + fontWeight: sortedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal), + ), + subtitle: Text('By ${sortedApps[index].app.author}', + style: TextStyle( + fontWeight: sortedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal)), trailing: sortedApps[index].downloadProgress != null ? Text( 'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%') @@ -256,8 +282,8 @@ class AppsPageState extends State { textAlign: TextAlign.end, )))), onTap: () { - if (selectedIds.isNotEmpty) { - toggleAppSelected(sortedApps[index].app.id); + if (selectedApps.isNotEmpty) { + toggleAppSelected(sortedApps[index].app); } else { Navigator.push( context, @@ -275,25 +301,25 @@ class AppsPageState extends State { children: [ IconButton( onPressed: () { - selectedIds.isEmpty - ? selectThese(sortedApps.map((e) => e.app.id).toList()) + selectedApps.isEmpty + ? selectThese(sortedApps.map((e) => e.app).toList()) : clearSelected(); }, icon: Icon( - selectedIds.isEmpty + selectedApps.isEmpty ? Icons.select_all_outlined : Icons.deselect_outlined, color: Theme.of(context).colorScheme.primary, ), - tooltip: selectedIds.isEmpty + tooltip: selectedApps.isEmpty ? 'Select All' - : 'Deselect ${selectedIds.length.toString()}'), + : 'Deselect ${selectedApps.length.toString()}'), const VerticalDivider(), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - selectedIds.isEmpty + selectedApps.isEmpty ? const SizedBox() : IconButton( visualDensity: VisualDensity.compact, @@ -307,11 +333,12 @@ class AppsPageState extends State { 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.', + '${selectedApps.length} App${selectedApps.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedApps.length == 1 ? 'it' : 'them'} manually.', ); }).then((values) { if (values != null) { - appsProvider.removeApps(selectedIds.toList()); + appsProvider.removeApps( + selectedApps.map((e) => e.id).toList()); } }); }, @@ -347,7 +374,7 @@ class AppsPageState extends State { builder: (BuildContext ctx) { return GeneratedFormModal( title: - 'Install${selectedIds.isEmpty ? ' ' : ' Selected '}Apps?', + 'Install${selectedApps.isEmpty ? ' ' : ' Selected '}Apps?', message: '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.', items: formInputs, @@ -386,11 +413,11 @@ class AppsPageState extends State { }); }, tooltip: - 'Install/Update${selectedIds.isEmpty ? ' ' : ' Selected '}Apps', + 'Install/Update${selectedApps.isEmpty ? ' ' : ' Selected '}Apps', icon: const Icon( Icons.file_download_outlined, )), - selectedIds.isEmpty + selectedApps.isEmpty ? const SizedBox() : IconButton( visualDensity: VisualDensity.compact, @@ -419,7 +446,7 @@ class AppsPageState extends State { ctx) { return AlertDialog( title: Text( - 'Mark ${selectedIds.length} Selected Apps as Updated?'), + 'Mark ${selectedApps.length} Selected Apps as Updated?'), content: const Text( 'Only applies to installed but out of date Apps.'), @@ -438,9 +465,7 @@ class AppsPageState extends State { HapticFeedback .selectionClick(); appsProvider - .saveApps(selectedIds.map((e) { - var a = - appsProvider.apps[e]!.app; + .saveApps(selectedApps.map((a) { if (a.installedVersion != null) { a.installedVersion = a.latestVersion; @@ -455,23 +480,50 @@ class AppsPageState extends State { 'Yes')) ], ); - }); + }).whenComplete(() { + Navigator.of( + context) + .pop(); + }); }, tooltip: 'Mark Selected Apps as Updated', icon: const Icon(Icons.done)), + IconButton( + onPressed: () { + var pinStatus = selectedApps + .where((element) => + element.pinned) + .isEmpty; + appsProvider.saveApps( + selectedApps.map((e) { + e.pinned = pinStatus; + return e; + }).toList()); + Navigator.of(context).pop(); + }, + tooltip: + '${selectedApps.where((element) => element.pinned).isEmpty ? 'Pin to' : 'Unpin from'} top', + icon: Icon(selectedApps + .where((element) => + element.pinned) + .isEmpty + ? Icons.bookmark_outline_rounded + : Icons + .bookmark_remove_outlined), + ), IconButton( onPressed: () { String urls = ''; - for (var id in selectedIds) { - urls += - '${appsProvider.apps[id]!.app.url}\n'; + for (var a in selectedApps) { + urls += '${a.url}\n'; } urls = urls.substring( 0, urls.length - 1); Share.share(urls, subject: - '${selectedIds.length} Selected App URLs from Obtainium'); + '${selectedApps.length} Selected App URLs from Obtainium'); + Navigator.of(context).pop(); }, tooltip: 'Share Selected App URLs', icon: const Icon(Icons.share), diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 13e3fa8..76db693 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -490,7 +490,8 @@ class AppsProvider with ChangeNotifier { currentApp.url, currentApp.additionalData, name: currentApp.name, - id: currentApp.id); + id: currentApp.id, + pinned: currentApp.pinned); newApp.installedVersion = currentApp.installedVersion; if (currentApp.preferredApkIndex < newApp.apkUrls.length) { newApp.preferredApkIndex = currentApp.preferredApkIndex; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 06c2c46..dd885de 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -40,6 +40,7 @@ class App { late int preferredApkIndex; late List additionalData; late DateTime? lastUpdateCheck; + bool pinned = false; App( this.id, this.url, @@ -50,11 +51,12 @@ class App { this.apkUrls, this.preferredApkIndex, this.additionalData, - this.lastUpdateCheck); + this.lastUpdateCheck, + this.pinned); @override String toString() { - return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}'; + return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; } factory App.fromJson(Map json) => App( @@ -75,7 +77,8 @@ class App { : List.from(jsonDecode(json['additionalData'])), json['lastUpdateCheck'] == null ? null - : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck'])); + : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), + json['pinned'] ?? false); Map toJson() => { 'id': id, @@ -87,7 +90,8 @@ class App { 'apkUrls': jsonEncode(apkUrls), 'preferredApkIndex': preferredApkIndex, 'additionalData': jsonEncode(additionalData), - 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch + 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, + 'pinned': pinned }; } @@ -224,7 +228,7 @@ class SourceProvider { } Future getApp(AppSource source, String url, List additionalData, - {String name = '', String? id}) async { + {String name = '', String? id, bool pinned = false}) async { String standardUrl = source.standardizeURL(preStandardizeUrl(url)); AppNames names = source.getAppNames(standardUrl); APKDetails apk = @@ -241,7 +245,8 @@ class SourceProvider { apk.apkUrls, apk.apkUrls.length - 1, additionalData, - DateTime.now()); + DateTime.now(), + pinned); } // Returns errors in [results, errors] instead of throwing them