Initial third party F-Droid repo support

Plus various bugfixes
And version increment
This commit is contained in:
Imran Remtulla
2022-12-15 21:22:03 -05:00
parent 6d0cac5894
commit f6ca5d42e8
19 changed files with 197 additions and 101 deletions

View File

@@ -15,6 +15,7 @@ Currently supported App sources:
- [Signal](https://signal.org/) - [Signal](https://signal.org/)
- [SourceForge](https://sourceforge.net/) - [SourceForge](https://sourceforge.net/)
- [APKMirror](https://apkmirror.com/) (Track-Only) - [APKMirror](https://apkmirror.com/) (Track-Only)
- Third Party F-Droid Repos (URLs ending with `/fdroid/repo`)
## Limitations ## Limitations
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected. - App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.

View File

@@ -20,7 +20,7 @@
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
"githubPATHint": "PAT must be in this format: username:token", "githubPATHint": "PAT must be in this format: username:token",
"githubPATFormat": "username:token", "githubPATFormat": "username:token",
"githubPATLinkText": "'About GitHub PATs", "githubPATLinkText": "About GitHub PATs",
"includePrereleases": "Include prereleases", "includePrereleases": "Include prereleases",
"fallbackToOlderReleases": "Fallback to older releases", "fallbackToOlderReleases": "Fallback to older releases",
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
@@ -179,7 +179,11 @@
"lastUpdateCheckX": "Last Update Check: {}", "lastUpdateCheckX": "Last Update Check: {}",
"remove": "Remove", "remove": "Remove",
"removeAppQuestion": "Remove App?", "removeAppQuestion": "Remove App?",
"yesMarkUpdated": "'Yes, Mark as Updated", "yesMarkUpdated": "Yes, Mark as Updated",
"fdroid": "F-Droid",
"appId": "App ID",
"reposHaveMultipleApps": "Repos may contain multiple Apps",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Too many requests (rate limited) - try again in {} minute", "one": "Too many requests (rate limited) - try again in {} minute",
"other": "Too many requests (rate limited) - try again in {} minutes" "other": "Too many requests (rate limited) - try again in {} minutes"

View File

@@ -179,7 +179,11 @@
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
"remove": "Rimuovi", "remove": "Rimuovi",
"removeAppQuestion": "Rimuovere App?", "removeAppQuestion": "Rimuovere App?",
"yesMarkUpdated": "'Sì, contrassegna come aggiornato", "yesMarkUpdated": "Sì, contrassegna come aggiornato",
"fdroid": "F-Droid",
"appId": "App ID",
"reposHaveMultipleApps": "Repos may contain multiple Apps",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"

View File

@@ -20,7 +20,7 @@
"githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)", "githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)",
"githubPATHint": "个人访问令牌必须为: username:token 形式", "githubPATHint": "个人访问令牌必须为: username:token 形式",
"githubPATFormat": "username:token", "githubPATFormat": "username:token",
"githubPATLinkText": "'关于 GitHub 个人访问令牌", "githubPATLinkText": "关于 GitHub 个人访问令牌",
"includePrereleases": "包含预发布版", "includePrereleases": "包含预发布版",
"fallbackToOlderReleases": "回落到旧版", "fallbackToOlderReleases": "回落到旧版",
"filterReleaseTitlesByRegEx": "通过正则表达式过滤发布标题", "filterReleaseTitlesByRegEx": "通过正则表达式过滤发布标题",
@@ -179,7 +179,11 @@
"lastUpdateCheckX": "Last Update Check: {}", "lastUpdateCheckX": "Last Update Check: {}",
"remove": "Remove", "remove": "Remove",
"removeAppQuestion": "Remove App?", "removeAppQuestion": "Remove App?",
"yesMarkUpdated": "'Yes, Mark as Updated", "yesMarkUpdated": "Yes, Mark as Updated",
"fdroid": "F-Droid",
"appId": "App ID",
"reposHaveMultipleApps": "Repos may contain multiple Apps",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "请求过多 (API 限制) - 在 {} 分钟后重试", "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
"other": "请求过多 (API 限制) - 在 {} 分钟后重试" "other": "请求过多 (API 限制) - 在 {} 分钟后重试"

View File

@@ -14,7 +14,7 @@ class APKMirror extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+'); RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -43,13 +43,12 @@ class APKMirror extends AppSource {
if (version == null || version.isEmpty) { if (version == null || version.isEmpty) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, []); return APKDetails(version, [], getAppNames(standardUrl));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
} }
@override
AppNames getAppNames(String standardUrl) { AppNames getAppNames(String standardUrl) {
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/'); List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');

View File

@@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -7,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
class FDroid extends AppSource { class FDroid extends AppSource {
FDroid() { FDroid() {
host = 'f-droid.org'; host = 'f-droid.org';
name = tr('fdroid');
} }
@override @override
@@ -20,7 +22,7 @@ class FDroid extends AppSource {
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+'); RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase()); match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -29,12 +31,13 @@ class FDroid extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
String? tryInferringAppId(String standardUrl) { String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) {
return Uri.parse(standardUrl).pathSegments.last; return Uri.parse(standardUrl).pathSegments.last;
} }
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix) { Response res, String apkUrlPrefix, String standardUrl) {
if (res.statusCode == 200) { if (res.statusCode == 200) {
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
if (releases.isEmpty) { if (releases.isEmpty) {
@@ -48,7 +51,8 @@ class FDroid extends AppSource {
.where((element) => element['versionName'] == latestVersion) .where((element) => element['versionName'] == latestVersion)
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList(); .toList();
return APKDetails(latestVersion, apkUrls); return APKDetails(latestVersion, apkUrls,
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
@@ -61,11 +65,7 @@ class FDroid extends AppSource {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),
'https://f-droid.org/repo/$appId'); 'https://f-droid.org/repo/$appId',
} standardUrl);
@override
AppNames getAppNames(String standardUrl) {
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
} }
} }

View File

@@ -0,0 +1,81 @@
import 'package:easy_localization/easy_localization.dart';
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 FDroidRepo extends AppSource {
FDroidRepo() {
name = tr('fdroidThirdPartyRepo');
additionalSourceAppSpecificFormItems = [
[
GeneratedFormItem(
label: tr('appId'),
hint: tr('reposHaveMultipleApps'),
required: true,
key: 'appId')
]
];
}
@override
String standardizeURL(String url) {
RegExp standardUrlRegExp =
RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)');
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) {
return findGeneratedFormValueByKey(
additionalSourceAppSpecificFormItems
.reduce((value, element) => [...value, ...element]),
additionalData,
'appId');
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData,
{bool trackOnly = false}) async {
String? appId =
tryInferringAppId(standardUrl, additionalData: additionalData);
if (appId == null) {
throw NoReleasesError();
}
var res = await get(Uri.parse('$standardUrl/index.xml'));
if (res.statusCode == 200) {
var body = parse(res.body);
var foundApps = body
.querySelectorAll('application')
.where((element) => element.attributes['id'] == appId)
.toList();
if (foundApps.isEmpty) {
throw NoReleasesError();
}
var authorName = body.querySelector('repo')?.attributes['name'] ?? name;
var appName = foundApps[0].querySelector('name')?.innerHtml ?? appId;
var releases = foundApps[0].querySelectorAll('package');
String? latestVersion = releases[0].querySelector('version')?.innerHtml;
if (latestVersion == null) {
throw NoVersionError();
}
List<String> apkUrls = releases
.where((element) =>
element.querySelector('version')?.innerHtml == latestVersion &&
element.querySelector('apkname') != null)
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
.toList();
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName));
} else {
throw NoReleasesError();
}
}
}

View File

@@ -90,7 +90,7 @@ class GitHub extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -162,14 +162,14 @@ class GitHub extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, targetRelease['apkUrls'] as List<String>); return APKDetails(version, targetRelease['apkUrls'] as List<String>,
getAppNames(standardUrl));
} else { } else {
rateLimitErrorCheck(res); rateLimitErrorCheck(res);
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
} }
@override
AppNames getAppNames(String standardUrl) { AppNames getAppNames(String standardUrl) {
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/'); List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');

View File

@@ -14,7 +14,7 @@ class GitLab extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -56,15 +56,9 @@ class GitLab extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, apkUrls); return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
} }
@override
AppNames getAppNames(String standardUrl) {
// Same as GitHub
return GitHub().getAppNames(standardUrl);
}
} }

View File

@@ -13,7 +13,7 @@ class IzzyOnDroid extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+'); RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -22,7 +22,8 @@ class IzzyOnDroid extends AppSource {
String? changeLogPageFromStandardUrl(String standardUrl) => null; String? changeLogPageFromStandardUrl(String standardUrl) => null;
@override @override
String? tryInferringAppId(String standardUrl) { String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) {
return FDroid().tryInferringAppId(standardUrl); return FDroid().tryInferringAppId(standardUrl);
} }
@@ -34,11 +35,7 @@ class IzzyOnDroid extends AppSource {
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
await get( await get(
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')), Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
'https://android.izzysoft.de/frepo/$appId'); 'https://android.izzysoft.de/frepo/$appId',
} standardUrl);
@override
AppNames getAppNames(String standardUrl) {
return AppNames('IzzyOnDroid', Uri.parse(standardUrl).pathSegments.last);
} }
} }

View File

@@ -13,7 +13,7 @@ class Mullvad extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host'); RegExp standardUrlRegEx = RegExp('^https?://$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -38,14 +38,11 @@ class Mullvad extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails( return APKDetails(
version, ['https://mullvad.net/download/app/apk/latest']); version,
['https://mullvad.net/download/app/apk/latest'],
AppNames(name, 'Mullvad-VPN'));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
} }
@override
AppNames getAppNames(String standardUrl) {
return AppNames('Mullvad-VPN', 'Mullvad-VPN');
}
} }

View File

@@ -30,12 +30,9 @@ class Signal extends AppSource {
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();
} }
return APKDetails(version, apkUrls); return APKDetails(version, apkUrls, AppNames(name, 'Signal'));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
} }
@override
AppNames getAppNames(String standardUrl) => AppNames('Signal', 'Signal');
} }

View File

@@ -13,7 +13,7 @@ class SourceForge extends AppSource {
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+'); RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(runtimeType.toString()); throw InvalidURLError(name);
} }
return url.substring(0, match.end); return url.substring(0, match.end);
} }
@@ -50,15 +50,13 @@ class SourceForge extends AppSource {
apkUrlListAllReleases // This can be used skipped for fallback support later apkUrlListAllReleases // This can be used skipped for fallback support later
.where((element) => getVersion(element) == version) .where((element) => getVersion(element) == version)
.toList(); .toList();
return APKDetails(version, apkUrlList); return APKDetails(
version,
apkUrlList,
AppNames(
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
} }
@override
AppNames getAppNames(String standardUrl) {
return AppNames(runtimeType.toString(),
standardUrl.substring(standardUrl.lastIndexOf('/') + 1));
}
} }

View File

@@ -28,7 +28,11 @@ class GeneratedFormItem {
this.belowWidgets = const [], this.belowWidgets = const [],
this.hint, this.hint,
this.opts, this.opts,
this.key = 'default'}); this.key = 'default'}) {
if (type != FormItemType.string) {
required = false;
}
}
} }
class GeneratedForm extends StatefulWidget { class GeneratedForm extends StatefulWidget {

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.8.12'; const String currentVersion = '0.8.13';
const String currentReleaseTag = const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@@ -87,7 +87,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
if (e is RateLimitError || e is SocketException) { if (e is RateLimitError || e is SocketException) {
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15; var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes, logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
args: [e.runtimeType.toString(), remainingMinutes.toString()])); args: [(e as AppSource).name, remainingMinutes.toString()]));
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes), AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: { Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch 'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch

View File

@@ -41,12 +41,12 @@ class _AddAppPageState extends State<AddAppPage> {
userInput = input; userInput = input;
fn() { fn() {
var source = valid ? sourceProvider.getSource(userInput) : null; var source = valid ? sourceProvider.getSource(userInput) : null;
if (pickedSource != source) { if (pickedSource.runtimeType != source.runtimeType) {
pickedSource = source; pickedSource = source;
sourceSpecificAdditionalData = sourceSpecificAdditionalData =
source != null ? source.additionalSourceAppSpecificDefaults : []; source != null ? source.additionalSourceAppSpecificDefaults : [];
sourceSpecificDataIsValid = source != null sourceSpecificDataIsValid = source != null
? sourceProvider.ifSourceAppsRequireAdditionalData(source) ? !sourceProvider.ifSourceAppsRequireAdditionalData(source)
: true; : true;
} }
} }
@@ -308,10 +308,8 @@ class _AddAppPageState extends State<AddAppPage> {
height: 64, height: 64,
), ),
Text( Text(
tr('additionalOptsFor', args: [ tr('additionalOptsFor',
pickedSource?.runtimeType.toString() ?? args: [pickedSource?.name ?? tr('source')]),
tr('source')
]),
style: TextStyle( style: TextStyle(
color: color:
Theme.of(context).colorScheme.primary)), Theme.of(context).colorScheme.primary)),
@@ -383,16 +381,20 @@ class _AddAppPageState extends State<AddAppPage> {
), ),
...sourceProvider.sources ...sourceProvider.sources
.map((e) => GestureDetector( .map((e) => GestureDetector(
onTap: () { onTap: e.host != null
launchUrlString('https://${e.host}', ? () {
mode: launchUrlString(
LaunchMode.externalApplication); 'https://${e.host}',
}, mode: LaunchMode
.externalApplication);
}
: null,
child: Text( child: Text(
'${e.runtimeType.toString()}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
style: const TextStyle( style: TextStyle(
decoration: decoration: e.host != null
TextDecoration.underline, ? TextDecoration.underline
: TextDecoration.none,
fontStyle: FontStyle.italic), fontStyle: FontStyle.italic),
))) )))
.toList() .toList()

View File

@@ -232,9 +232,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
return GeneratedFormModal( return GeneratedFormModal(
title: tr('searchX', title: tr('searchX',
args: [ args: [
source source.name
.runtimeType
.toString()
]), ]),
items: [ items: [
[ [
@@ -319,9 +317,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
}); });
}); });
}, },
child: Text(tr('searchX', args: [ child: Text(
source.runtimeType.toString() tr('searchX', args: [source.name])))
])))
])) ]))
.toList(), .toList(),
...sourceProvider.massUrlSources ...sourceProvider.massUrlSources

View File

@@ -8,6 +8,7 @@ import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidRepo.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/izzyondroid.dart';
@@ -28,8 +29,9 @@ class AppNames {
class APKDetails { class APKDetails {
late String version; late String version;
late List<String> apkUrls; late List<String> apkUrls;
late AppNames names;
APKDetails(this.version, this.apkUrls); APKDetails(this.version, this.apkUrls, this.names);
} }
class App { class App {
@@ -106,11 +108,11 @@ class App {
// Ensure the input is starts with HTTPS and has no WWW // Ensure the input is starts with HTTPS and has no WWW
preStandardizeUrl(String url) { preStandardizeUrl(String url) {
if (url.toLowerCase().indexOf('http://') != 0 && url = url.toLowerCase();
url.toLowerCase().indexOf('https://') != 0) { if (url.indexOf('http://') != 0 && url.indexOf('https://') != 0) {
url = 'https://$url'; url = 'https://$url';
} }
if (url.toLowerCase().indexOf('https://www.') == 0) { if (url.indexOf('https://www.') == 0) {
url = 'https://${url.substring(12)}'; url = 'https://${url.substring(12)}';
} }
url = url url = url
@@ -135,8 +137,14 @@ List<String> getLinksFromParsedHTML(
.toList(); .toList();
class AppSource { class AppSource {
late String host; String? host;
late String name;
bool enforceTrackOnly = false; bool enforceTrackOnly = false;
AppSource() {
name = runtimeType.toString();
}
String standardizeURL(String url) { String standardizeURL(String url) {
throw NotImplementedError(); throw NotImplementedError();
} }
@@ -147,10 +155,6 @@ class AppSource {
throw NotImplementedError(); throw NotImplementedError();
} }
AppNames getAppNames(String standardUrl) {
throw NotImplementedError();
}
// Different Sources may need different kinds of additional data for Apps // Different Sources may need different kinds of additional data for Apps
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = []; List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = [];
List<String> additionalSourceAppSpecificDefaults = []; List<String> additionalSourceAppSpecificDefaults = [];
@@ -168,7 +172,7 @@ class AppSource {
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = []; List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
String? changeLogPageFromStandardUrl(String standardUrl) { String? changeLogPageFromStandardUrl(String standardUrl) {
throw NotImplementedError(); return null;
} }
Future<String> apkUrlPrefetchModifier(String apkUrl) async { Future<String> apkUrlPrefetchModifier(String apkUrl) async {
@@ -180,7 +184,8 @@ class AppSource {
throw NotImplementedError(); throw NotImplementedError();
} }
String? tryInferringAppId(String standardUrl) { String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) {
return null; return null;
} }
} }
@@ -206,7 +211,8 @@ class SourceProvider {
Mullvad(), Mullvad(),
Signal(), Signal(),
SourceForge(), SourceForge(),
APKMirror() APKMirror(),
FDroidRepo()
]; ];
// Add more mass url source classes here so they are available via the service // Add more mass url source classes here so they are available via the service
@@ -215,12 +221,23 @@ class SourceProvider {
AppSource getSource(String url) { AppSource getSource(String url) {
url = preStandardizeUrl(url); url = preStandardizeUrl(url);
AppSource? source; AppSource? source;
for (var s in sources) { for (var s in sources.where((element) => element.host != null)) {
if (url.toLowerCase().contains('://${s.host}')) { if (url.contains('://${s.host}')) {
source = s; source = s;
break; break;
} }
} }
if (source == null) {
for (var s in sources.where((element) => element.host == null)) {
try {
s.standardizeURL(url);
source = s;
break;
} catch (e) {
//
}
}
}
if (source == null) { if (source == null) {
throw UnsupportedURLError(); throw UnsupportedURLError();
} }
@@ -252,7 +269,7 @@ class SourceProvider {
return false; return false;
} }
} }
return sources.map((e) => e.host).contains(parts.last); return true;
} }
Future<App> getApp(AppSource source, String url, List<String> additionalData, Future<App> getApp(AppSource source, String url, List<String> additionalData,
@@ -262,7 +279,6 @@ class SourceProvider {
bool trackOnly = false, bool trackOnly = false,
String? installedVersion}) async { String? installedVersion}) async {
String standardUrl = source.standardizeURL(preStandardizeUrl(url)); String standardUrl = source.standardizeURL(preStandardizeUrl(url));
AppNames names = source.getAppNames(standardUrl);
APKDetails apk = await source APKDetails apk = await source
.getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly); .getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly);
if (apk.apkUrls.isEmpty && !trackOnly) { if (apk.apkUrls.isEmpty && !trackOnly) {
@@ -271,13 +287,14 @@ class SourceProvider {
String apkVersion = apk.version.replaceAll('/', '-'); String apkVersion = apk.version.replaceAll('/', '-');
return App( return App(
id ?? id ??
source.tryInferringAppId(standardUrl) ?? source.tryInferringAppId(standardUrl,
generateTempID(names, source), additionalData: additionalData) ??
generateTempID(apk.names, source),
standardUrl, standardUrl,
names.author[0].toUpperCase() + names.author.substring(1), apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
name.trim().isNotEmpty name.trim().isNotEmpty
? name ? name
: names.name[0].toUpperCase() + names.name.substring(1), : apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
installedVersion, installedVersion,
apkVersion, apkVersion,
apk.apkUrls, apk.apkUrls,

View File

@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 0.8.12+76 # When changing this, update the tag in main() accordingly version: 0.8.13+77 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'