From 5db2c5f0b1fbfde72101622b15ea4958e98b3543 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 11 Nov 2022 21:44:20 -0500 Subject: [PATCH 1/5] Initial changes to support search --- lib/app_sources/fdroid.dart | 16 ++---- lib/app_sources/github.dart | 92 ++++++++++++++++-------------- lib/app_sources/gitlab.dart | 17 ++---- lib/app_sources/izzyondroid.dart | 17 ++---- lib/app_sources/mullvad.dart | 17 ++---- lib/app_sources/signal.dart | 17 ++---- lib/app_sources/sourceforge.dart | 17 ++---- lib/custom_errors.dart | 4 ++ lib/providers/source_provider.dart | 37 +++++++++--- 9 files changed, 105 insertions(+), 129 deletions(-) diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index d40a45f..25ad2fc 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -4,9 +4,10 @@ import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; -class FDroid implements AppSource { - @override - late String host = 'f-droid.org'; +class FDroid extends AppSource { + FDroid() { + host = 'f-droid.org'; + } @override String standardizeURL(String url) { @@ -77,13 +78,4 @@ class FDroid implements AppSource { AppNames getAppNames(String standardUrl) { return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last); } - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 932c0fd..e5d4651 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -7,9 +7,52 @@ import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class GitHub implements AppSource { - @override - late String host = 'github.com'; +class GitHub extends AppSource { + GitHub() { + host = 'github.com'; + + additionalDataDefaults = ['true', 'true', '']; + + moreSourceSettingsFormItems = [ + GeneratedFormItem( + label: 'GitHub Personal Access Token (Increases Rate Limit)', + id: 'github-creds', + required: false, + additionalValidators: [ + (value) { + if (value != null && value.trim().isNotEmpty) { + if (value + .split(':') + .where((element) => element.trim().isNotEmpty) + .length != + 2) { + return 'PAT must be in this format: username:token'; + } + } + return null; + } + ], + hint: 'username:token', + belowWidgets: [ + const SizedBox( + height: 8, + ), + GestureDetector( + onTap: () { + launchUrlString( + 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', + mode: LaunchMode.externalApplication); + }, + child: const Text( + 'About GitHub PATs', + style: TextStyle( + decoration: TextDecoration.underline, fontSize: 12), + )) + ]) + ]; + + canSearch = true; + } @override String standardizeURL(String url) { @@ -142,44 +185,7 @@ class GitHub implements AppSource { ]; @override - List additionalDataDefaults = ['true', 'true', '']; - - @override - List moreSourceSettingsFormItems = [ - GeneratedFormItem( - label: 'GitHub Personal Access Token (Increases Rate Limit)', - id: 'github-creds', - required: false, - additionalValidators: [ - (value) { - if (value != null && value.trim().isNotEmpty) { - if (value - .split(':') - .where((element) => element.trim().isNotEmpty) - .length != - 2) { - return 'PAT must be in this format: username:token'; - } - } - return null; - } - ], - hint: 'username:token', - belowWidgets: [ - const SizedBox( - height: 8, - ), - GestureDetector( - onTap: () { - launchUrlString( - 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', - mode: LaunchMode.externalApplication); - }, - child: const Text( - 'About GitHub PATs', - style: TextStyle( - decoration: TextDecoration.underline, fontSize: 12), - )) - ]) - ]; + Future> search(String query) async { + return []; + } } diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index b8b9094..9fe7fad 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -1,13 +1,13 @@ import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:obtainium/app_sources/github.dart'; -import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; -class GitLab implements AppSource { - @override - late String host = 'gitlab.com'; +class GitLab extends AppSource { + GitLab() { + host = 'gitlab.com'; + } @override String standardizeURL(String url) { @@ -72,13 +72,4 @@ class GitLab implements AppSource { // Same as GitHub return GitHub().getAppNames(standardUrl); } - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index 0e77915..b18b3ff 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -1,12 +1,12 @@ 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'; -class IzzyOnDroid implements AppSource { - @override - late String host = 'android.izzysoft.de'; +class IzzyOnDroid extends AppSource { + IzzyOnDroid() { + host = 'android.izzysoft.de'; + } @override String standardizeURL(String url) { @@ -63,13 +63,4 @@ class IzzyOnDroid implements AppSource { AppNames getAppNames(String standardUrl) { return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last); } - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index a402104..660deb8 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -1,12 +1,12 @@ 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'; -class Mullvad implements AppSource { - @override - late String host = 'mullvad.net'; +class Mullvad extends AppSource { + Mullvad() { + host = 'mullvad.net'; + } @override String standardizeURL(String url) { @@ -50,13 +50,4 @@ class Mullvad implements AppSource { AppNames getAppNames(String standardUrl) { return AppNames('Mullvad-VPN', 'Mullvad-VPN'); } - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/app_sources/signal.dart b/lib/app_sources/signal.dart index 250a688..1b10a32 100644 --- a/lib/app_sources/signal.dart +++ b/lib/app_sources/signal.dart @@ -1,12 +1,12 @@ import 'dart:convert'; 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'; -class Signal implements AppSource { - @override - late String host = 'signal.org'; +class Signal extends AppSource { + Signal() { + host = 'signal.org'; + } @override String standardizeURL(String url) { @@ -42,13 +42,4 @@ class Signal implements AppSource { @override AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal'); - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index 7114bf6..1373e80 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -1,12 +1,12 @@ 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'; -class SourceForge implements AppSource { - @override - late String host = 'sourceforge.net'; +class SourceForge extends AppSource { + SourceForge() { + host = 'sourceforge.net'; + } @override String standardizeURL(String url) { @@ -66,13 +66,4 @@ class SourceForge implements AppSource { return AppNames(runtimeType.toString(), standardUrl.substring(standardUrl.lastIndexOf('/') + 1)); } - - @override - List> additionalDataFormItems = []; - - @override - List additionalDataDefaults = []; - - @override - List moreSourceSettingsFormItems = []; } diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index b1d4f3f..3b3a568 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -48,6 +48,10 @@ class IDChangedError extends ObtainiumError { : super('Downloaded package ID does not match existing App ID'); } +class NotImplementedError extends ObtainiumError { + NotImplementedError() : super('This class has not implemented this function'); +} + class MultiAppMultiError extends ObtainiumError { Map> content = {}; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index aec6c70..06c2c46 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -128,17 +128,36 @@ List getLinksFromParsedHTML( .map((e) => '$prependToLinks${e.attributes['href']!}') .toList(); -abstract class AppSource { +class AppSource { late String host; - String standardizeURL(String url); + String standardizeURL(String url) { + throw NotImplementedError(); + } + Future getLatestAPKDetails( - String standardUrl, List additionalData); - AppNames getAppNames(String standardUrl); - late List> additionalDataFormItems; - late List additionalDataDefaults; - late List moreSourceSettingsFormItems; - String? changeLogPageFromStandardUrl(String standardUrl); - Future apkUrlPrefetchModifier(String apkUrl); + String standardUrl, List additionalData) { + throw NotImplementedError(); + } + + AppNames getAppNames(String standardUrl) { + throw NotImplementedError(); + } + + List> additionalDataFormItems = []; + List additionalDataDefaults = []; + List moreSourceSettingsFormItems = []; + String? changeLogPageFromStandardUrl(String standardUrl) { + throw NotImplementedError(); + } + + Future apkUrlPrefetchModifier(String apkUrl) { + throw NotImplementedError(); + } + + bool canSearch = false; + Future> search(String query) { + throw NotImplementedError(); + } } abstract class MassAppUrlSource { From ab57b978758bea74782774e01fd857b586b9cf0f Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 12 Nov 2022 01:05:16 -0500 Subject: [PATCH 2/5] Ready to implement GitHub search (UI done) --- lib/pages/import_export.dart | 224 ++++++++++++++++++++++++----------- 1 file changed, 152 insertions(+), 72 deletions(-) diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index d919447..82dbfee 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -223,6 +223,97 @@ class _ImportExportPageState extends State { child: const Text( 'Import from URL List', )), + ...sourceProvider.sources + .where((element) => element.canSearch) + .map((source) => Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress + ? null + : () { + () async { + var values = await showDialog< + List>( + context: context, + builder: + (BuildContext ctx) { + return GeneratedFormModal( + title: + 'Search ${source.runtimeType}', + items: [ + [ + GeneratedFormItem( + label: + '${source.runtimeType} Search Query') + ] + ], + defaultValues: const [], + ); + }); + if (values != null && + values[0].isNotEmpty) { + setState(() { + importInProgress = true; + }); + var urls = await source + .search(values[0]); + if (urls.isNotEmpty) { + var selectedUrls = + await showDialog< + List< + String>?>( + context: context, + builder: + (BuildContext + ctx) { + return UrlSelectionModal( + urls: urls); + }); + if (selectedUrls != + null && + selectedUrls + .isNotEmpty) { + var errors = + await addApps( + selectedUrls); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showError( + 'Imported ${selectedUrls.length} Apps', + context); + } else { + showDialog( + context: context, + builder: + (BuildContext + ctx) { + return ImportErrorDialog( + urlsLength: + selectedUrls + .length, + errors: + errors); + }); + } + } + } + } + }() + .catchError((e) { + showError(e, context); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + }, + child: Text( + 'Search ${source.runtimeType}')) + ])) + .toList(), ...sourceProvider.massUrlSources .map((source) => Column( crossAxisAlignment: @@ -233,84 +324,73 @@ class _ImportExportPageState extends State { onPressed: importInProgress ? null : () { - showDialog( - context: context, - builder: - (BuildContext ctx) { - return GeneratedFormModal( - title: - 'Import ${source.name}', - items: source - .requiredArgs - .map((e) => [ - GeneratedFormItem( - label: e) - ]) - .toList(), - defaultValues: const [], - ); - }).then((values) { + () async { + var values = await showDialog( + context: context, + builder: + (BuildContext ctx) { + return GeneratedFormModal( + title: + 'Import ${source.name}', + items: + source + .requiredArgs + .map( + (e) => [ + GeneratedFormItem(label: e) + ]) + .toList(), + defaultValues: const [], + ); + }); if (values != null) { setState(() { importInProgress = true; }); - source - .getUrls(values) - .then((urls) { - showDialog?>( - context: context, - builder: - (BuildContext - ctx) { - return UrlSelectionModal( - urls: urls); - }) - .then((selectedUrls) { - if (selectedUrls != - null) { - addApps(selectedUrls) - .then((errors) { - if (errors - .isEmpty) { - showError( - 'Imported ${selectedUrls.length} Apps', - context); - } else { - showDialog( - context: - context, - builder: - (BuildContext - ctx) { - return ImportErrorDialog( - urlsLength: - selectedUrls - .length, - errors: - errors); - }); - } - }).whenComplete(() { - setState(() { - importInProgress = - false; + var urls = await source + .getUrls(values); + var selectedUrls = + await showDialog< + List?>( + context: context, + builder: + (BuildContext + ctx) { + return UrlSelectionModal( + urls: urls); }); - }); - } else { - setState(() { - importInProgress = - false; - }); - } - }); - }).catchError((e) { - setState(() { - importInProgress = - false; - }); - showError(e, context); - }); + if (selectedUrls != null) { + var errors = + await addApps( + selectedUrls); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showError( + 'Imported ${selectedUrls.length} Apps', + context); + } else { + showDialog( + context: context, + builder: + (BuildContext + ctx) { + return ImportErrorDialog( + urlsLength: + selectedUrls + .length, + errors: + errors); + }); + } + } } + }() + .catchError((e) { + showError(e, context); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); }); }, child: Text('Import ${source.name}')) From 905a807ee90dddeecaea68d44373f451cd5bf617 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 12 Nov 2022 01:25:32 -0500 Subject: [PATCH 3/5] GitHub search added --- lib/app_sources/github.dart | 75 ++++++++++++++++++++++-------------- lib/custom_errors.dart | 8 ++-- lib/pages/import_export.dart | 14 +++++-- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index e5d4651..8ec3a80 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -51,6 +51,35 @@ class GitHub extends AppSource { ]) ]; + additionalDataFormItems = [ + [ + GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool) + ], + [ + GeneratedFormItem( + label: 'Fallback to older releases', type: FormItemType.bool) + ], + [ + GeneratedFormItem( + label: 'Filter Release Titles by Regular Expression', + type: FormItemType.string, + required: false, + additionalValidators: [ + (value) { + if (value == null || value.isEmpty) { + return null; + } + try { + RegExp(value); + } catch (e) { + return 'Invalid regular expression'; + } + return null; + } + ]) + ] + ]; + canSearch = true; } @@ -156,36 +185,24 @@ class GitHub extends AppSource { return AppNames(names[0], names[1]); } - @override - List> additionalDataFormItems = [ - [GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)], - [ - GeneratedFormItem( - label: 'Fallback to older releases', type: FormItemType.bool) - ], - [ - GeneratedFormItem( - label: 'Filter Release Titles by Regular Expression', - type: FormItemType.string, - required: false, - additionalValidators: [ - (value) { - if (value == null || value.isEmpty) { - return null; - } - try { - RegExp(value); - } catch (e) { - return 'Invalid regular expression'; - } - return null; - } - ]) - ] - ]; - @override Future> search(String query) async { - return []; + Response res = await get(Uri.parse( + 'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100')); + if (res.statusCode == 200) { + return (jsonDecode(res.body)['items'] as List) + .map((e) => e['html_url'] as String) + .toList(); + } else { + if (res.headers['x-ratelimit-remaining'] == '0') { + throw RateLimitError( + (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / + 60000000) + .round()); + } + throw ObtainiumError( + res.reasonPhrase ?? 'Error ${res.statusCode.toString()}', + unexpected: true); + } } } diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index 3b3a568..a1a9902 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:obtainium/providers/apps_provider.dart'; class ObtainiumError { late String message; - ObtainiumError(this.message); + bool unexpected; + ObtainiumError(this.message, {this.unexpected = false}); @override String toString() { return message; @@ -55,7 +55,7 @@ class NotImplementedError extends ObtainiumError { class MultiAppMultiError extends ObtainiumError { Map> content = {}; - MultiAppMultiError() : super('Multiple Errors Placeholder'); + MultiAppMultiError() : super('Multiple Errors Placeholder', unexpected: true); add(String appId, String string) { var tempIds = content.remove(string); @@ -75,7 +75,7 @@ class MultiAppMultiError extends ObtainiumError { } showError(dynamic e, BuildContext context) { - if (e is String || (e is ObtainiumError && e is! MultiAppMultiError)) { + if (e is String || (e is ObtainiumError && !e.unexpected)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), ); diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 82dbfee..56d261d 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -270,7 +270,10 @@ class _ImportExportPageState extends State { (BuildContext ctx) { return UrlSelectionModal( - urls: urls); + urls: urls, + defaultSelected: + false, + ); }); if (selectedUrls != null && @@ -299,6 +302,9 @@ class _ImportExportPageState extends State { }); } } + } else { + throw ObtainiumError( + 'No results found'); } } }() @@ -470,9 +476,11 @@ class _ImportErrorDialogState extends State { // ignore: must_be_immutable class UrlSelectionModal extends StatefulWidget { - UrlSelectionModal({super.key, required this.urls}); + UrlSelectionModal( + {super.key, required this.urls, this.defaultSelected = true}); List urls; + bool defaultSelected; @override State createState() => _UrlSelectionModalState(); @@ -484,7 +492,7 @@ class _UrlSelectionModalState extends State { void initState() { super.initState(); for (var url in widget.urls) { - urlSelections.putIfAbsent(url, () => true); + urlSelections.putIfAbsent(url, () => widget.defaultSelected); } } From 9bd7ddb21b56c9c1b9aa85d80de85ce7e6eeae1b Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 12 Nov 2022 02:14:45 -0500 Subject: [PATCH 4/5] 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 From e2bf83498182ae789676c470a48734c7a343ac98 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 12 Nov 2022 02:15:23 -0500 Subject: [PATCH 5/5] Increment version --- lib/main.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f69cf1a..4838d8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; -const String currentVersion = '0.6.11'; +const String currentVersion = '0.7.0'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/pubspec.yaml b/pubspec.yaml index 1d47478..bb02ef9 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.6.11+55 # When changing this, update the tag in main() accordingly +version: 0.7.0+56 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0'