From fb9e66332df81a8f27a7bdcb69e7c2da2bb26dd0 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 5 May 2023 22:35:32 -0400 Subject: [PATCH 1/7] APKPure, SourceHut, Bugfixes --- lib/app_sources/apkcombo.dart | 116 ++++++++++++++++++++ lib/app_sources/apkmirror.dart | 2 +- lib/app_sources/apkpure.dart | 69 ++++++++++++ lib/app_sources/codeberg.dart | 1 + lib/app_sources/fdroid.dart | 4 +- lib/app_sources/fdroidrepo.dart | 3 +- lib/app_sources/github.dart | 5 +- lib/app_sources/gitlab.dart | 3 +- lib/app_sources/html.dart | 170 +++++++++++++++-------------- lib/app_sources/izzyondroid.dart | 4 +- lib/app_sources/jenkins.dart | 6 +- lib/app_sources/mullvad.dart | 2 +- lib/app_sources/neutroncode.dart | 2 +- lib/app_sources/signal.dart | 2 +- lib/app_sources/sourceforge.dart | 3 +- lib/app_sources/sourcehut.dart | 101 +++++++++++++++++ lib/app_sources/steammobile.dart | 2 +- lib/app_sources/telegramapp.dart | 2 +- lib/app_sources/vlc.dart | 6 +- lib/app_sources/whatsapp.dart | 16 +-- lib/pages/add_app.dart | 6 +- lib/providers/apps_provider.dart | 20 ++-- lib/providers/source_provider.dart | 22 +++- 23 files changed, 445 insertions(+), 122 deletions(-) create mode 100644 lib/app_sources/apkcombo.dart create mode 100644 lib/app_sources/apkpure.dart create mode 100644 lib/app_sources/sourcehut.dart diff --git a/lib/app_sources/apkcombo.dart b/lib/app_sources/apkcombo.dart new file mode 100644 index 0000000..6c5dfa6 --- /dev/null +++ b/lib/app_sources/apkcombo.dart @@ -0,0 +1,116 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class APKCombo extends AppSource { + APKCombo() { + host = 'apkcombo.com'; + } + + @override + String sourceSpecificStandardizeURL(String url) { + RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+'); + var match = standardUrlRegEx.firstMatch(url.toLowerCase()); + if (match == null) { + throw InvalidURLError(name); + } + return url.substring(0, match.end); + } + + @override + String? tryInferringAppId(String standardUrl, + {Map additionalSettings = const {}}) { + return Uri.parse(standardUrl).pathSegments.last; + } + + @override + Map get requestHeaders => { + "User-Agent": "curl/8.0.1", + "Accept": "*/*", + "Connection": "keep-alive", + "Host": "$host" + }; + + Future>> getApkUrls(String standardUrl) async { + var res = await sourceRequest('$standardUrl/download/apk'); + if (res.statusCode != 200) { + throw getObtainiumHttpError(res); + } + var html = parse(res.body); + return html + .querySelectorAll('#variants-tab > div > ul > li') + .map((e) { + String? arch = e + .querySelector('code') + ?.text + .trim() + .replaceAll(',', '') + .replaceAll(':', '-') + .replaceAll(' ', '-'); + return e.querySelectorAll('a').map((e) { + String? url = e.attributes['href']; + if (url != null && + !Uri.parse(url).path.toLowerCase().endsWith('.apk')) { + url = null; + } + String verCode = + e.querySelector('.info .header .vercode')?.text.trim() ?? ''; + return MapEntry( + arch != null ? '$arch-$verCode.apk' : '', url ?? ''); + }).toList(); + }) + .reduce((value, element) => [...value, ...element]) + .where((element) => element.value.isNotEmpty) + .toList(); + } + + @override + Future apkUrlPrefetchModifier( + String apkUrl, String standardUrl) async { + var freshURLs = await getApkUrls(standardUrl); + var path2Match = Uri.parse(apkUrl).path; + for (var url in freshURLs) { + if (Uri.parse(url.value).path == path2Match) { + return url.value; + } + } + throw NoAPKError(); + } + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + String appId = tryInferringAppId(standardUrl)!; + String host = Uri.parse(standardUrl).host; + var preres = await sourceRequest(standardUrl); + if (preres.statusCode != 200) { + throw getObtainiumHttpError(preres); + } + var res = parse(preres.body); + String? version = res.querySelector('div.version')?.text.trim(); + if (version == null) { + throw NoVersionError(); + } + String appName = res.querySelector('div.app_name')?.text.trim() ?? appId; + String author = res.querySelector('div.author')?.text.trim() ?? appName; + List infoArray = res + .querySelectorAll('div.information-table > .item > div.value') + .map((e) => e.text.trim()) + .toList(); + DateTime? releaseDate; + if (infoArray.length >= 2) { + try { + releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]); + } catch (e) { + // ignore + } + } + return APKDetails( + version, await getApkUrls(standardUrl), AppNames(author, appName), + releaseDate: releaseDate); + } +} diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index 396e599..2265cc7 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -57,7 +57,7 @@ class APKMirror extends AppSource { true ? additionalSettings['filterReleaseTitlesByRegEx'] : null; - Response res = await get(Uri.parse('$standardUrl/feed')); + Response res = await sourceRequest('$standardUrl/feed'); if (res.statusCode == 200) { var items = parse(res.body).querySelectorAll('item'); dynamic targetRelease; diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart new file mode 100644 index 0000000..437ae4c --- /dev/null +++ b/lib/app_sources/apkpure.dart @@ -0,0 +1,69 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class APKPure extends AppSource { + APKPure() { + host = 'apkpure.com'; + } + + @override + String sourceSpecificStandardizeURL(String url) { + RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+'); + RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); + if (match != null) { + url = 'https://$host/${Uri.parse(url).path}'; + } + RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+'); + match = standardUrlRegExA.firstMatch(url.toLowerCase()); + if (match == null) { + throw InvalidURLError(name); + } + return url.substring(0, match.end); + } + + @override + String? tryInferringAppId(String standardUrl, + {Map additionalSettings = const {}}) { + return Uri.parse(standardUrl).pathSegments.last; + } + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + String appId = tryInferringAppId(standardUrl)!; + String host = Uri.parse(standardUrl).host; + var res = await sourceRequest('$standardUrl/download'); + if (res.statusCode == 200) { + var html = parse(res.body); + String? version = html.querySelector('span.info-sdk span')?.text.trim(); + if (version == null) { + throw NoVersionError(); + } + String? dateString = + html.querySelector('span.info-other span.date')?.text.trim(); + DateTime? releaseDate = dateString != null + ? DateFormat('MMMM d, yyyy').parse(dateString) + : null; + List> apkUrls = [ + MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest') + ]; + String author = html + .querySelector('span.info-sdk') + ?.text + .trim() + .substring(version.length + 4) ?? + Uri.parse(standardUrl).pathSegments.reversed.last; + String appName = + html.querySelector('h1.info-title')?.text.trim() ?? appId; + return APKDetails(version, apkUrls, AppNames(author, appName), + releaseDate: releaseDate); + } else { + throw getObtainiumHttpError(res); + } + } +} diff --git a/lib/app_sources/codeberg.dart b/lib/app_sources/codeberg.dart index a45d620..d50332d 100644 --- a/lib/app_sources/codeberg.dart +++ b/lib/app_sources/codeberg.dart @@ -9,6 +9,7 @@ import 'package:obtainium/providers/source_provider.dart'; class Codeberg extends AppSource { Codeberg() { host = 'codeberg.org'; + overrideEligible = true; additionalSourceSpecificSettingFormItems = []; diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 318a983..88b4f5e 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -66,14 +66,14 @@ class FDroid extends AppSource { String? appId = tryInferringAppId(standardUrl); String host = Uri.parse(standardUrl).host; return getAPKUrlsFromFDroidPackagesAPIResponse( - await get(Uri.parse('https://$host/api/v1/packages/$appId')), + await sourceRequest('https://$host/api/v1/packages/$appId'), 'https://$host/repo/$appId', standardUrl); } @override Future>> search(String query) async { - Response res = await get(Uri.parse('https://search.$host/?q=$query')); + Response res = await sourceRequest('https://search.$host/?q=$query'); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; parse(res.body).querySelectorAll('.package-header').forEach((e) { diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 33c8edf..184e139 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -8,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart'; class FDroidRepo extends AppSource { FDroidRepo() { name = tr('fdroidThirdPartyRepo'); + overrideEligible = true; additionalSourceAppSpecificSettingFormItems = [ [ @@ -28,7 +29,7 @@ class FDroidRepo extends AppSource { if (appIdOrName == null) { throw NoReleasesError(); } - var res = await get(Uri.parse('$standardUrl/index.xml')); + var res = await sourceRequest('$standardUrl/index.xml'); if (res.statusCode == 200) { var body = parse(res.body); var foundApps = body.querySelectorAll('application').where((element) { diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index e6b67ae..a9a76c3 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class GitHub extends AppSource { GitHub() { host = 'github.com'; + overrideEligible = true; additionalSourceSpecificSettingFormItems = [ GeneratedFormTextField('github-creds', @@ -108,7 +109,7 @@ class GitHub extends AppSource { true ? additionalSettings['filterReleaseTitlesByRegEx'] : null; - Response res = await get(Uri.parse(requestUrl)); + Response res = await sourceRequest(requestUrl); if (res.statusCode == 200) { var releases = jsonDecode(res.body) as List; @@ -216,7 +217,7 @@ class GitHub extends AppSource { Future>> searchCommon( String query, String requestUrl, String rootProp, {Function(Response)? onHttpErrorCode}) async { - Response res = await get(Uri.parse(requestUrl)); + Response res = await sourceRequest(requestUrl); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; for (var e in (jsonDecode(res.body)[rootProp] as List)) { diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 150cb04..07f8369 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -9,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart'; class GitLab extends AppSource { GitLab() { host = 'gitlab.com'; + overrideEligible = true; additionalSourceAppSpecificSettingFormItems = [ [ @@ -39,7 +40,7 @@ class GitLab extends AppSource { ) async { bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; - Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); + Response res = await sourceRequest('$standardUrl/-/tags?format=atom'); if (res.statusCode == 200) { var standardUri = Uri.parse(standardUrl); var parsedHtml = parse(res.body); diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index c4c039f..d9ec173 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -4,84 +4,109 @@ import 'package:http/http.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; +String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { + try { + Uri.parse(ambiguousUrl).origin; + return ambiguousUrl; + } catch (err) { + // is relative + } + var currPathSegments = referenceAbsoluteUrl.path + .split('/') + .where((element) => element.trim().isNotEmpty) + .toList(); + if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { + return '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; + } else if (ambiguousUrl.split('/').length == 1) { + return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl'; + } else { + return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl'; + } +} + +int compareAlphaNumeric(String a, String b) { + List aParts = _splitAlphaNumeric(a); + List bParts = _splitAlphaNumeric(b); + + for (int i = 0; i < aParts.length && i < bParts.length; i++) { + String aPart = aParts[i]; + String bPart = bParts[i]; + + bool aIsNumber = _isNumeric(aPart); + bool bIsNumber = _isNumeric(bPart); + + if (aIsNumber && bIsNumber) { + int aNumber = int.parse(aPart); + int bNumber = int.parse(bPart); + int cmp = aNumber.compareTo(bNumber); + if (cmp != 0) { + return cmp; + } + } else if (!aIsNumber && !bIsNumber) { + int cmp = aPart.compareTo(bPart); + if (cmp != 0) { + return cmp; + } + } else { + // Alphanumeric strings come before numeric strings + return aIsNumber ? 1 : -1; + } + } + + return aParts.length.compareTo(bParts.length); +} + +List _splitAlphaNumeric(String s) { + List parts = []; + StringBuffer sb = StringBuffer(); + + bool isNumeric = _isNumeric(s[0]); + sb.write(s[0]); + + for (int i = 1; i < s.length; i++) { + bool currentIsNumeric = _isNumeric(s[i]); + if (currentIsNumeric == isNumeric) { + sb.write(s[i]); + } else { + parts.add(sb.toString()); + sb.clear(); + sb.write(s[i]); + isNumeric = currentIsNumeric; + } + } + + parts.add(sb.toString()); + + return parts; +} + +bool _isNumeric(String s) { + return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57; +} + class HTML extends AppSource { + HTML() { + overrideEligible = true; + } + @override String sourceSpecificStandardizeURL(String url) { return url; } - int compareAlphaNumeric(String a, String b) { - List aParts = _splitAlphaNumeric(a); - List bParts = _splitAlphaNumeric(b); - - for (int i = 0; i < aParts.length && i < bParts.length; i++) { - String aPart = aParts[i]; - String bPart = bParts[i]; - - bool aIsNumber = _isNumeric(aPart); - bool bIsNumber = _isNumeric(bPart); - - if (aIsNumber && bIsNumber) { - int aNumber = int.parse(aPart); - int bNumber = int.parse(bPart); - int cmp = aNumber.compareTo(bNumber); - if (cmp != 0) { - return cmp; - } - } else if (!aIsNumber && !bIsNumber) { - int cmp = aPart.compareTo(bPart); - if (cmp != 0) { - return cmp; - } - } else { - // Alphanumeric strings come before numeric strings - return aIsNumber ? 1 : -1; - } - } - - return aParts.length.compareTo(bParts.length); - } - - List _splitAlphaNumeric(String s) { - List parts = []; - StringBuffer sb = StringBuffer(); - - bool isNumeric = _isNumeric(s[0]); - sb.write(s[0]); - - for (int i = 1; i < s.length; i++) { - bool currentIsNumeric = _isNumeric(s[i]); - if (currentIsNumeric == isNumeric) { - sb.write(s[i]); - } else { - parts.add(sb.toString()); - sb.clear(); - sb.write(s[i]); - isNumeric = currentIsNumeric; - } - } - - parts.add(sb.toString()); - - return parts; - } - - bool _isNumeric(String s) { - return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57; - } - @override Future getLatestAPKDetails( String standardUrl, Map additionalSettings, ) async { var uri = Uri.parse(standardUrl); - Response res = await get(uri); + Response res = await sourceRequest(standardUrl); if (res.statusCode == 200) { List links = parse(res.body) .querySelectorAll('a') .map((element) => element.attributes['href'] ?? '') - .where((element) => element.toLowerCase().endsWith('.apk')) + .where((element) => + Uri.parse(element).path.toLowerCase().endsWith('.apk')) .toList(); links.sort( (a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last)); @@ -95,25 +120,8 @@ class HTML extends AppSource { var rel = links.last; var apkName = rel.split('/').last; var version = apkName.substring(0, apkName.length - 4); - List apkUrls = [rel].map((e) { - try { - Uri.parse(e).origin; - return e; - } catch (err) { - // is relative - } - var currPathSegments = uri.path - .split('/') - .where((element) => element.trim().isNotEmpty) - .toList(); - if (e.startsWith('/') || currPathSegments.isEmpty) { - return '${uri.origin}/$e'; - } else if (e.split('/').length == 1) { - return '${uri.origin}/${currPathSegments.join('/')}/$e'; - } else { - return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e'; - } - }).toList(); + List apkUrls = + [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); return APKDetails( version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app'))); } else { diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index 0fbceb9..cdab17a 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -31,8 +31,8 @@ class IzzyOnDroid extends AppSource { ) async { String? appId = tryInferringAppId(standardUrl); return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( - await get( - Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')), + await sourceRequest( + 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), 'https://android.izzysoft.de/frepo/$appId', standardUrl); } diff --git a/lib/app_sources/jenkins.dart b/lib/app_sources/jenkins.dart index 366b76d..804b3fb 100644 --- a/lib/app_sources/jenkins.dart +++ b/lib/app_sources/jenkins.dart @@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart'; class Jenkins extends AppSource { Jenkins() { + overrideEligible = true; overrideVersionDetectionFormDefault('releaseDateAsVersion', true); } @@ -30,7 +31,7 @@ class Jenkins extends AppSource { ) async { standardUrl = trimJobUrl(standardUrl); Response res = - await get(Uri.parse('$standardUrl/lastSuccessfulBuild/api/json')); + await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json'); if (res.statusCode == 200) { var json = jsonDecode(res.body); var releaseDate = json['timestamp'] == null @@ -55,9 +56,6 @@ class Jenkins extends AppSource { .where((url) => url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk')) .toList(); - if (apkUrls.isEmpty) { - throw NoAPKError(); - } return APKDetails( version, apkUrls, diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index 64ef092..4e15d2f 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -28,7 +28,7 @@ class Mullvad extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('$standardUrl/en/download/android')); + Response res = await sourceRequest('$standardUrl/en/download/android'); if (res.statusCode == 200) { var versions = parse(res.body) .querySelectorAll('p') diff --git a/lib/app_sources/neutroncode.dart b/lib/app_sources/neutroncode.dart index 45374a9..4fbec3c 100644 --- a/lib/app_sources/neutroncode.dart +++ b/lib/app_sources/neutroncode.dart @@ -78,7 +78,7 @@ class NeutronCode extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse(standardUrl)); + Response res = await sourceRequest(standardUrl); if (res.statusCode == 200) { var http = parse(res.body); var name = http.querySelector('.pd-title')?.innerHtml; diff --git a/lib/app_sources/signal.dart b/lib/app_sources/signal.dart index 40d6953..f7bda25 100644 --- a/lib/app_sources/signal.dart +++ b/lib/app_sources/signal.dart @@ -19,7 +19,7 @@ class Signal extends AppSource { Map additionalSettings, ) async { Response res = - await get(Uri.parse('https://updates.$host/android/latest.json')); + await sourceRequest('https://updates.$host/android/latest.json'); if (res.statusCode == 200) { var json = jsonDecode(res.body); String? apkUrl = json['url']; diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index ee2f22b..794eef1 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart'; class SourceForge extends AppSource { SourceForge() { host = 'sourceforge.net'; + overrideEligible = true; } @override @@ -29,7 +30,7 @@ class SourceForge extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('$standardUrl/rss?path=/')); + Response res = await sourceRequest('$standardUrl/rss?path=/'); if (res.statusCode == 200) { var parsedHtml = parse(res.body); var allDownloadLinks = diff --git a/lib/app_sources/sourcehut.dart b/lib/app_sources/sourcehut.dart new file mode 100644 index 0000000..b85758f --- /dev/null +++ b/lib/app_sources/sourcehut.dart @@ -0,0 +1,101 @@ +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:obtainium/app_sources/github.dart'; +import 'package:obtainium/app_sources/html.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; +import 'package:obtainium/components/generated_form.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class SourceHut extends AppSource { + SourceHut() { + host = 'git.sr.ht'; + overrideEligible = true; + + additionalSourceAppSpecificSettingFormItems = [ + [ + GeneratedFormSwitch('fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), defaultValue: true) + ] + ]; + } + + @override + String sourceSpecificStandardizeURL(String url) { + RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); + RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + if (match == null) { + throw InvalidURLError(name); + } + return url.substring(0, match.end); + } + + @override + String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl; + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + Uri standardUri = Uri.parse(standardUrl); + String appName = standardUri.pathSegments.last; + bool fallbackToOlderReleases = + additionalSettings['fallbackToOlderReleases'] == true; + Response res = await sourceRequest('$standardUrl/refs/rss.xml'); + if (res.statusCode == 200) { + var parsedHtml = parse(res.body); + List apkDetailsList = []; + int ind = 0; + + for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) { + // Limit 5 for speed + if (!fallbackToOlderReleases && ind > 0) { + break; + } + String? version = entry.querySelector('title')?.text.trim(); + if (version == null) { + throw NoVersionError(); + } + String? releaseDateString = entry.querySelector('pubDate')?.innerHtml; + var link = entry.querySelector('link'); + String releasePage = '$standardUrl/refs/$version'; + DateTime? releaseDate = releaseDateString != null + ? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString) + : null; + var res2 = await sourceRequest(releasePage); + List> apkUrls = []; + if (res2.statusCode == 200) { + apkUrls = getApkUrlsFromUrls(parse(res2.body) + .querySelectorAll('a') + .map((e) => e.attributes['href'] ?? '') + .where((e) => e.toLowerCase().endsWith('.apk')) + .map((e) => ensureAbsoluteUrl(e, standardUri)) + .toList()); + } + apkDetailsList.add(APKDetails( + version, + apkUrls, + AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName, + appName), + releaseDate: releaseDate)); + ind++; + } + if (apkDetailsList.isEmpty) { + throw NoReleasesError(); + } + if (fallbackToOlderReleases) { + if (additionalSettings['trackOnly'] != true) { + apkDetailsList = + apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList(); + } + if (apkDetailsList.isEmpty) { + throw NoReleasesError(); + } + } + return apkDetailsList.first; + } else { + throw getObtainiumHttpError(res); + } + } +} diff --git a/lib/app_sources/steammobile.dart b/lib/app_sources/steammobile.dart index 305c89a..65c518b 100644 --- a/lib/app_sources/steammobile.dart +++ b/lib/app_sources/steammobile.dart @@ -29,7 +29,7 @@ class SteamMobile extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('https://$host/mobile')); + Response res = await sourceRequest('https://$host/mobile'); if (res.statusCode == 200) { var apkNamePrefix = additionalSettings['app'] as String?; if (apkNamePrefix == null) { diff --git a/lib/app_sources/telegramapp.dart b/lib/app_sources/telegramapp.dart index 3c02156..44042e0 100644 --- a/lib/app_sources/telegramapp.dart +++ b/lib/app_sources/telegramapp.dart @@ -20,7 +20,7 @@ class TelegramApp extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK')); + Response res = await sourceRequest('https://t.me/s/TAndroidAPK'); if (res.statusCode == 200) { var http = parse(res.body); var messages = diff --git a/lib/app_sources/vlc.dart b/lib/app_sources/vlc.dart index b105068..06ad878 100644 --- a/lib/app_sources/vlc.dart +++ b/lib/app_sources/vlc.dart @@ -19,8 +19,8 @@ class VLC extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get( - Uri.parse('https://www.videolan.org/vlc/download-android.html')); + Response res = await sourceRequest( + 'https://www.videolan.org/vlc/download-android.html'); if (res.statusCode == 200) { var dwUrlBase = 'get.videolan.org/vlc-android'; var dwLinks = parse(res.body) @@ -38,7 +38,7 @@ class VLC extends AppSource { throw NoVersionError(); } String? targetUrl = 'https://$dwUrlBase/$version/'; - Response res2 = await get(Uri.parse(targetUrl)); + Response res2 = await sourceRequest(targetUrl); String mirrorDwBase = 'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/'; List apkUrls = []; diff --git a/lib/app_sources/whatsapp.dart b/lib/app_sources/whatsapp.dart index 85deb84..f17d8a0 100644 --- a/lib/app_sources/whatsapp.dart +++ b/lib/app_sources/whatsapp.dart @@ -14,21 +14,21 @@ class WhatsApp extends AppSource { } @override - Future apkUrlPrefetchModifier(String apkUrl) async { - Response res = await get(Uri.parse('https://www.whatsapp.com/android')); + Future apkUrlPrefetchModifier( + String apkUrl, String standardUrl) async { + Response res = await sourceRequest('https://www.whatsapp.com/android'); if (res.statusCode == 200) { var targetLinks = parse(res.body) .querySelectorAll('a') - .map((e) => e.attributes['href']) - .where((e) => e != null) + .map((e) => e.attributes['href'] ?? '') + .where((e) => e.isNotEmpty) .where((e) => - e!.contains('scontent.whatsapp.net') && - e.contains('WhatsApp.apk')) + e.contains('content.whatsapp.net') && e.contains('WhatsApp.apk')) .toList(); if (targetLinks.isEmpty) { throw NoAPKError(); } - return targetLinks[0]!; + return targetLinks[0]; } else { throw getObtainiumHttpError(res); } @@ -39,7 +39,7 @@ class WhatsApp extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('https://www.whatsapp.com/android')); + Response res = await sourceRequest('https://www.whatsapp.com/android'); if (res.statusCode == 200) { var targetElements = parse(res.body) .querySelectorAll('p') diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index b0654c3..3cef280 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -302,8 +302,10 @@ class _AddAppPageState extends State { 'overrideSource', defaultValue: HTML().runtimeType.toString(), [ - ...sourceProvider.sources.map( - (s) => MapEntry(s.runtimeType.toString(), s.name)) + ...sourceProvider.sources + .where((s) => s.overrideEligible) + .map((s) => + MapEntry(s.runtimeType.toString(), s.name)) ], label: tr('overrideSource')) ] diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 9cc445c..e4f611c 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -125,10 +125,13 @@ class AppsProvider with ChangeNotifier { } downloadFile(String url, String fileName, Function? onProgress, - {bool useExisting = true}) async { + {bool useExisting = true, Map? headers}) async { var destDir = (await getExternalCacheDirectories())!.first.path; - StreamedResponse response = - await Client().send(Request('GET', Uri.parse(url))); + var req = Request('GET', Uri.parse(url)); + if (headers != null) { + req.headers.addAll(headers); + } + StreamedResponse response = await Client().send(req); File downloadedFile = File('$destDir/$fileName'); if (!(downloadedFile.existsSync() && useExisting)) { File tempDownloadedFile = File('${downloadedFile.path}.part'); @@ -170,15 +173,16 @@ class AppsProvider with ChangeNotifier { notifyListeners(); } try { - String downloadUrl = await SourceProvider() - .getSource(app.url, overrideSource: app.overrideSource) - .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value); + AppSource source = SourceProvider() + .getSource(app.url, overrideSource: app.overrideSource); + String downloadUrl = await source.apkUrlPrefetchModifier( + app.apkUrls[app.preferredApkIndex].value, app.url); var fileName = '${app.id}-${downloadUrl.hashCode}.apk'; var notif = DownloadNotification(app.finalName, 100); notificationsProvider?.cancel(notif.id); int? prevProg; - File downloadedFile = - await downloadFile(downloadUrl, fileName, (double? progress) { + File downloadedFile = await downloadFile(downloadUrl, fileName, + headers: source.requestHeaders, (double? progress) { int? prog = progress?.ceil(); if (apps[app.id] != null) { apps[app.id]!.downloadProgress = progress; diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 556c294..f498496 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -7,7 +7,9 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:html/dom.dart'; import 'package:http/http.dart'; +import 'package:obtainium/app_sources/apkcombo.dart'; import 'package:obtainium/app_sources/apkmirror.dart'; +import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/app_sources/codeberg.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart'; @@ -20,6 +22,7 @@ import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/neutroncode.dart'; import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/sourceforge.dart'; +import 'package:obtainium/app_sources/sourcehut.dart'; import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/vlc.dart'; @@ -315,6 +318,7 @@ abstract class AppSource { late String name; bool enforceTrackOnly = false; bool changeLogIfAnyIsMarkDown = true; + bool overrideEligible = false; AppSource() { name = runtimeType.toString(); @@ -344,6 +348,18 @@ abstract class AppSource { return url; } + Map? get requestHeaders => null; + + Future sourceRequest(String url) async { + if (requestHeaders != null) { + var req = Request('GET', Uri.parse(url)); + req.headers.addAll(requestHeaders!); + return Response.fromStream(await Client().send(req)); + } else { + return get(Uri.parse(url)); + } + } + String sourceSpecificStandardizeURL(String url) { throw NotImplementedError(); } @@ -410,7 +426,8 @@ abstract class AppSource { return null; } - Future apkUrlPrefetchModifier(String apkUrl) async { + Future apkUrlPrefetchModifier( + String apkUrl, String standardUrl) async { return apkUrl; } @@ -459,7 +476,10 @@ class SourceProvider { FDroidRepo(), Jenkins(), SourceForge(), + SourceHut(), APKMirror(), + APKPure(), + // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) Mullvad(), Signal(), VLC(), From 30c89fe3850db494263cc1f251f1b7b92140d668 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 5 May 2023 23:08:34 -0400 Subject: [PATCH 2/7] Option to move non-installed apps to bottom (#264) --- assets/translations/de.json | 1 + assets/translations/en.json | 1 + assets/translations/es.json | 1 + assets/translations/fa.json | 1 + assets/translations/fr.json | 1 + assets/translations/hu.json | 1 + assets/translations/it.json | 1 + assets/translations/ja.json | 1 + assets/translations/zh.json | 1 + lib/pages/apps.dart | 12 +++++++++++ lib/pages/settings.dart | 30 ++++++++++++++++++++++------ lib/providers/settings_provider.dart | 9 +++++++++ 12 files changed, 54 insertions(+), 6 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index 1406cf1..6510dd8 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -228,6 +228,7 @@ "dontShowAgain": "Nicht noch einmal zeigen", "dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen", "dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "App entfernen?", "other": "Apps entfernen?" diff --git a/assets/translations/en.json b/assets/translations/en.json index 58878ff..606f66f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -228,6 +228,7 @@ "dontShowAgain": "Don't show this again", "dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/es.json b/assets/translations/es.json index 1f80bac..3d8f1ad 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -228,6 +228,7 @@ "dontShowAgain": "No mostrar de nuevo", "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'", "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "¿Eliminar Aplicación?", "other": "¿Eliminar Aplicaciones?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index d0ba5c5..33da610 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -228,6 +228,7 @@ "dontShowAgain": "دوباره این را نشان نده", "dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید", "dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "برنامه حذف شود؟", "other": "برنامه ها حذف شوند؟" diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 27dbd45..1840c8a 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -228,6 +228,7 @@ "dontShowAgain": "Don't show this again", "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "Supprimer l'application ?", "other": "Supprimer les applications ?" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index f1ed8d2..26de38e 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -227,6 +227,7 @@ "dontShowAgain": "Ne mutassa ezt újra", "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést", "dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "Eltávolítja az alkalmazást?", "other": "Eltávolítja az alkalmazást?" diff --git a/assets/translations/it.json b/assets/translations/it.json index 1b122d5..0e49191 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -228,6 +228,7 @@ "dontShowAgain": "Don't show this again", "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "Rimuovere l'App?", "other": "Rimuovere le App?" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index f7cbab3..7f5bf32 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -228,6 +228,7 @@ "dontShowAgain": "二度と表示しない", "dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない", "dontShowAPKOriginWarnings": "APK Originの警告を表示しない", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "アプリを削除しますか?", "other": "アプリを削除しますか?" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index a430229..91ca57d 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -228,6 +228,7 @@ "dontShowAgain": "Don't show this again", "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", + "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", "removeAppQuestion": { "one": "是否删除应用?", "other": "是否删除应用?" diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index c240676..6a0a947 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -185,6 +185,18 @@ class AppsPageState extends State { listedApps = [...temp, ...listedApps]; } + if (settingsProvider.buryNonInstalled) { + var temp = []; + listedApps = listedApps.where((sa) { + if (sa.app.installedVersion == null) { + temp.add(sa); + return false; + } + return true; + }).toList(); + listedApps = [...listedApps, ...temp]; + } + var tempPinned = []; var tempNotPinned = []; for (var a in listedApps) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 988fe85..224a076 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -227,7 +227,7 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('useBlackTheme')), + Flexible(child: Text(tr('useBlackTheme'))), Switch( value: settingsProvider.useBlackTheme, onChanged: (value) { @@ -254,7 +254,7 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('showWebInAppView')), + Flexible(child: Text(tr('showWebInAppView'))), Switch( value: settingsProvider.showAppWebpage, onChanged: (value) { @@ -266,7 +266,7 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('pinUpdates')), + Flexible(child: Text(tr('pinUpdates'))), Switch( value: settingsProvider.pinUpdates, onChanged: (value) { @@ -278,7 +278,21 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('groupByCategory')), + Flexible( + child: Text( + tr('moveNonInstalledAppsToBottom'))), + Switch( + value: settingsProvider.buryNonInstalled, + onChanged: (value) { + settingsProvider.buryNonInstalled = value; + }) + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('groupByCategory'))), Switch( value: settingsProvider.groupByCategory, onChanged: (value) { @@ -290,7 +304,9 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('dontShowTrackOnlyWarnings')), + Flexible( + child: + Text(tr('dontShowTrackOnlyWarnings'))), Switch( value: settingsProvider.hideTrackOnlyWarning, @@ -304,7 +320,9 @@ class _SettingsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(tr('dontShowAPKOriginWarnings')), + Flexible( + child: + Text(tr('dontShowAPKOriginWarnings'))), Switch( value: settingsProvider.hideAPKOriginWarning, diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 2f03dfb..162d7f9 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -154,6 +154,15 @@ class SettingsProvider with ChangeNotifier { notifyListeners(); } + bool get buryNonInstalled { + return prefs?.getBool('buryNonInstalled') ?? false; + } + + set buryNonInstalled(bool show) { + prefs?.setBool('buryNonInstalled', show); + notifyListeners(); + } + bool get groupByCategory { return prefs?.getBool('groupByCategory') ?? false; } From 8ca5964d31d74e21727fd52a13206c534dcf53ae Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Fri, 5 May 2023 23:09:40 -0400 Subject: [PATCH 3/7] Updated README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b21d416..d65389a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Currently supported App sources: - [Mullvad](https://mullvad.net/en/) - [Signal](https://signal.org/) - [SourceForge](https://sourceforge.net/) +- [SourceHut](https://git.sr.ht/) - [APKMirror](https://apkmirror.com/) (Track-Only) +- [APKPure](https://apkpure.com/) - Third Party F-Droid Repos - Jenkins Jobs - [Steam](https://store.steampowered.com/mobile) From b4642e16add024edc80ea0afc9008f576de3e507 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 6 May 2023 00:06:48 -0400 Subject: [PATCH 4/7] GitLab search (#422) + better settings UI --- assets/translations/de.json | 4 ++- assets/translations/en.json | 4 ++- assets/translations/es.json | 4 ++- assets/translations/fa.json | 4 ++- assets/translations/fr.json | 4 ++- assets/translations/hu.json | 4 ++- assets/translations/it.json | 4 ++- assets/translations/ja.json | 4 ++- assets/translations/zh.json | 4 ++- lib/app_sources/fdroid.dart | 3 +- lib/app_sources/github.dart | 9 ++++-- lib/app_sources/gitlab.dart | 63 +++++++++++++++++++++++++++++++++++++ lib/custom_errors.dart | 5 +++ lib/pages/add_app.dart | 15 +++++++-- lib/pages/settings.dart | 45 +++++++++++++------------- 15 files changed, 138 insertions(+), 38 deletions(-) diff --git a/assets/translations/de.json b/assets/translations/de.json index 6510dd8..fb3135a 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token", "githubPATFormat": "Benutzername:Token", - "githubPATLinkText": "Über GitHub PATs", "includePrereleases": "Vorabversionen einbeziehen", "fallbackToOlderReleases": "Fallback auf ältere Versionen", "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen", "dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "App entfernen?", "other": "Apps entfernen?" diff --git a/assets/translations/en.json b/assets/translations/en.json index 606f66f..dd027a9 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", "githubPATHint": "PAT must be in this format: username:token", "githubPATFormat": "username:token", - "githubPATLinkText": "About GitHub PATs", "includePrereleases": "Include prereleases", "fallbackToOlderReleases": "Fallback to older releases", "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "Remove App?", "other": "Remove Apps?" diff --git a/assets/translations/es.json b/assets/translations/es.json index 3d8f1ad..d3fae41 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -20,7 +20,6 @@ "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token", "githubPATFormat": "nombre_de_usuario:token", - "githubPATLinkText": "Sobre los TAP de GitHub", "includePrereleases": "Incluir versiones preliminares", "fallbackToOlderReleases": "Retorceder a versiones previas", "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'", "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "¿Eliminar Aplicación?", "other": "¿Eliminar Aplicaciones?" diff --git a/assets/translations/fa.json b/assets/translations/fa.json index 33da610..36e25d4 100644 --- a/assets/translations/fa.json +++ b/assets/translations/fa.json @@ -20,7 +20,6 @@ "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", "githubPATHint": "PAT باید در این قالب باشد: username:token", "githubPATFormat": "username:token", - "githubPATLinkText": "درباره گیتهاب PATs", "includePrereleases": "شامل نسخه های اولیه", "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید", "dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "برنامه حذف شود؟", "other": "برنامه ها حذف شوند؟" diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 1840c8a..4e29966 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -20,7 +20,6 @@ "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", "githubPATHint": "Le JAP doit être dans ce format : username:token", "githubPATFormat": "username:token", - "githubPATLinkText": "À propos des JAP GitHub", "includePrereleases": "Inclure les avant-premières", "fallbackToOlderReleases": "Retour aux anciennes versions", "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "Supprimer l'application ?", "other": "Supprimer les applications ?" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 26de38e..85134f0 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)", "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", "githubPATFormat": "felhasználónév:token", - "githubPATLinkText": "A GitHub PAT-okról", "includePrereleases": "Tartalmazza az előzetes kiadásokat", "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", @@ -228,6 +227,9 @@ "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést", "dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "Eltávolítja az alkalmazást?", "other": "Eltávolítja az alkalmazást?" diff --git a/assets/translations/it.json b/assets/translations/it.json index 0e49191..728bce8 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", "githubPATHint": "PAT deve seguire questo formato: username:token", "githubPATFormat": "username:token", - "githubPATLinkText": "Informazioni su GitHub PAT", "includePrereleases": "Includi prerelease", "fallbackToOlderReleases": "Ripiega su release precedenti", "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "Rimuovere l'App?", "other": "Rimuovere le App?" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 7f5bf32..02ee143 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", "githubPATFormat": "ユーザー名:トークン", - "githubPATLinkText": "GitHub PATsについて", "includePrereleases": "プレリリースを含む", "fallbackToOlderReleases": "旧リリースへのフォールバック", "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない", "dontShowAPKOriginWarnings": "APK Originの警告を表示しない", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "アプリを削除しますか?", "other": "アプリを削除しますか?" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 91ca57d..1cf5267 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -20,7 +20,6 @@ "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", "githubPATHint": "个人访问令牌必须为“username:token”的格式", "githubPATFormat": "username:token", - "githubPATLinkText": "关于 GitHub 个人访问令牌", "includePrereleases": "包含预发行版", "fallbackToOlderReleases": "将旧发行版作为备选", "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", @@ -229,6 +228,9 @@ "dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning", "dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings", "moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View", + "gitlabPATLabel": "GitLab Personal Access Token (Enables Search)", + "about": "About", + "requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "removeAppQuestion": { "one": "是否删除应用?", "other": "是否删除应用?" diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 88b4f5e..28d45cc 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -73,7 +73,8 @@ class FDroid extends AppSource { @override Future>> search(String query) async { - Response res = await sourceRequest('https://search.$host/?q=$query'); + Response res = await sourceRequest( + 'https://search.$host/?q=${Uri.encodeQueryComponent(query)}'); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; parse(res.body).querySelectorAll('.package-header').forEach((e) { diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index a9a76c3..f67c36e 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -35,7 +35,7 @@ class GitHub extends AppSource { hint: tr('githubPATFormat'), belowWidgets: [ const SizedBox( - height: 8, + height: 4, ), GestureDetector( onTap: () { @@ -44,10 +44,13 @@ class GitHub extends AppSource { mode: LaunchMode.externalApplication); }, child: Text( - tr('githubPATLinkText'), + tr('about'), style: const TextStyle( decoration: TextDecoration.underline, fontSize: 12), - )) + )), + const SizedBox( + height: 4, + ), ]) ]; diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 07f8369..ae65875 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -1,15 +1,47 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class GitLab extends AppSource { GitLab() { host = 'gitlab.com'; overrideEligible = true; + canSearch = true; + + additionalSourceSpecificSettingFormItems = [ + GeneratedFormTextField('gitlab-creds', + label: tr('gitlabPATLabel'), + password: true, + required: false, + belowWidgets: [ + const SizedBox( + height: 4, + ), + GestureDetector( + onTap: () { + launchUrlString( + 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token', + mode: LaunchMode.externalApplication); + }, + child: Text( + tr('about'), + style: const TextStyle( + decoration: TextDecoration.underline, fontSize: 12), + )), + const SizedBox( + height: 4, + ) + ]) + ]; additionalSourceAppSpecificSettingFormItems = [ [ @@ -29,6 +61,37 @@ class GitLab extends AppSource { return url.substring(0, match.end); } + Future getPATIfAny() async { + SettingsProvider settingsProvider = SettingsProvider(); + await settingsProvider.initializeSettings(); + String? creds = settingsProvider + .getSettingString(additionalSourceSpecificSettingFormItems[0].key); + return creds != null && creds.isNotEmpty ? creds : null; + } + + @override + Future>> search(String query) async { + String? PAT = await getPATIfAny(); + if (PAT == null) { + throw CredsNeededError(name); + } + var url = + 'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}'; + var res = await sourceRequest(url); + if (res.statusCode != 200) { + throw getObtainiumHttpError(res); + } + var json = jsonDecode(res.body) as List; + Map> results = {}; + json.forEach((element) { + results['https://$host/${element['path_with_namespace']}'] = [ + element['name_with_namespace'], + element['description'] ?? tr('noDescription') + ]; + }); + return results; + } + @override String? changeLogPageFromStandardUrl(String standardUrl) => '$standardUrl/-/releases'; diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index 99970ea..8f535cf 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -25,6 +25,11 @@ class InvalidURLError extends ObtainiumError { : super(tr('invalidURLForSource', args: [sourceName])); } +class CredsNeededError extends ObtainiumError { + CredsNeededError(String sourceName) + : super(tr('requiresCredentialsInSettings', args: [sourceName])); +} + class NoReleasesError extends ObtainiumError { NoReleasesError() : super(tr('noReleaseFound')); } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 3cef280..3f1886e 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -248,9 +248,18 @@ class _AddAppPageState extends State { searching = true; }); try { - var results = await Future.wait(sourceProvider.sources - .where((e) => e.canSearch) - .map((e) => e.search(searchQuery))); + var results = await Future.wait( + sourceProvider.sources.where((e) => e.canSearch).map((e) async { + try { + return await e.search(searchQuery); + } catch (err) { + if (err is! CredsNeededError) { + rethrow; + } else { + return >{}; + } + } + })); // .then((results) async { // Interleave results instead of simple reduce diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 224a076..65684cf 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -205,6 +205,10 @@ class _SettingsPageState extends State { height: 16, ); + const height32 = SizedBox( + height: 32, + ); + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView(slivers: [ @@ -217,9 +221,26 @@ class _SettingsPageState extends State { : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + tr('updates'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + intervalDropdown, + height32, + Text( + tr('sourceSpecific'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary), + ), + ...sourceSpecificFields, + height32, Text( tr('appearance'), style: TextStyle( + fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary), ), themeDropdown, @@ -332,31 +353,11 @@ class _SettingsPageState extends State { }) ], ), - const Divider( - height: 16, - ), - height16, - Text( - tr('updates'), - style: TextStyle( - color: Theme.of(context).colorScheme.primary), - ), - intervalDropdown, - const Divider( - height: 48, - ), - Text( - tr('sourceSpecific'), - style: TextStyle( - color: Theme.of(context).colorScheme.primary), - ), - ...sourceSpecificFields, - const Divider( - height: 48, - ), + height32, Text( tr('categories'), style: TextStyle( + fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary), ), height16, From 2e4fe89b85898c753506443b3012f5a526d78107 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 6 May 2023 00:12:25 -0400 Subject: [PATCH 5/7] APKPure bugfix, upgrade packages --- lib/app_sources/apkpure.dart | 2 +- pubspec.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart index 437ae4c..ac9155b 100644 --- a/lib/app_sources/apkpure.dart +++ b/lib/app_sources/apkpure.dart @@ -47,7 +47,7 @@ class APKPure extends AppSource { String? dateString = html.querySelector('span.info-other span.date')?.text.trim(); DateTime? releaseDate = dateString != null - ? DateFormat('MMMM d, yyyy').parse(dateString) + ? DateFormat('MMM dd, yyyy').parse(dateString) : null; List> apkUrls = [ MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest') diff --git a/pubspec.lock b/pubspec.lock index 0a80259..fc6d56b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,10 +94,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" csslib: dependency: transitive description: @@ -256,10 +256,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb" + sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" flutter_test: dependency: "direct dev" description: flutter @@ -623,10 +623,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f" + sha256: acf091c6e55c50d00b30b8532b2dd23e393cf775861665ebd0f15cdd6ebfb079 url: "https://pub.dev" source: hosted - version: "2.2.8" + version: "2.2.8+1" sqflite_common: dependency: transitive description: From 12867634b663a212ed36c02c245a7cafca2f46d8 Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 6 May 2023 00:13:05 -0400 Subject: [PATCH 6/7] 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 056f3cc..c7ca28b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.12.3'; +const String currentVersion = '0.13.0'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/pubspec.yaml b/pubspec.yaml index 1826128..d1197a8 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.12.3+163 # When changing this, update the tag in main() accordingly +version: 0.13.0+164 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0' From ee292146d128a00f6b6123a24df8c098245d052e Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 6 May 2023 00:30:46 -0400 Subject: [PATCH 7/7] Better GitHub release sorting in some cases (#534) --- lib/app_sources/github.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index f67c36e..c314d26 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -133,7 +135,7 @@ class GitHub extends AppSource { ? DateTime.parse(rel['published_at']) : null; releases.sort((a, b) { - // See #478 + // See #478 and #534 if (a == b) { return 0; } else if (a == null) { @@ -141,8 +143,19 @@ class GitHub extends AppSource { } else if (b == null) { return 1; } else { - return getReleaseDateFromRelease(a)! - .compareTo(getReleaseDateFromRelease(b)!); + var stdFormats = findStandardFormatsForVersion(a['tag_name'], true) + .intersection(findStandardFormatsForVersion(b['tag_name'], true)); + if (stdFormats.isNotEmpty) { + var reg = RegExp(stdFormats.first); + var matchA = reg.firstMatch(a['tag_name']); + var matchB = reg.firstMatch(b['tag_name']); + return compareAlphaNumeric( + (a['tag_name'] as String).substring(matchA!.start, matchA.end), + (b['tag_name'] as String).substring(matchB!.start, matchB.end)); + } else { + return getReleaseDateFromRelease(a)! + .compareTo(getReleaseDateFromRelease(b)!); + } } }); releases = releases.reversed.toList();