diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart new file mode 100644 index 0000000..3c6daec --- /dev/null +++ b/lib/app_sources/apkmirror.dart @@ -0,0 +1,55 @@ +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class APKMirror extends AppSource { + APKMirror() { + host = 'apkmirror.com'; + enforceTrackOnly = true; + } + + @override + String standardizeURL(String url) { + RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+'); + RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + if (match == null) { + throw InvalidURLError(runtimeType.toString()); + } + return url.substring(0, match.end); + } + + @override + String? changeLogPageFromStandardUrl(String standardUrl) => + '$standardUrl/#whatsnew'; + + @override + Future getLatestAPKDetails( + String standardUrl, List additionalData) async { + Response res = await get(Uri.parse('$standardUrl/feed')); + if (res.statusCode == 200) { + String? titleString = parse(res.body) + .querySelector('item') + ?.querySelector('title') + ?.innerHtml; + String? version = titleString + ?.substring(0, + RegExp(' build ( |[0-9])+').firstMatch(titleString)?.start ?? 0) + .split(' ') + .last; + if (version == null) { + throw NoVersionError(); + } + return APKDetails(version, []); + } else { + throw NoReleasesError(); + } + } + + @override + AppNames getAppNames(String standardUrl) { + String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); + List names = temp.substring(temp.indexOf('/') + 1).split('/'); + return AppNames(names[1], names[2]); + } +} diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 9750c99..7bc6448 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -143,7 +143,7 @@ class _AddAppPageState extends State { (BuildContext ctx) { return GeneratedFormModal( title: - 'App is Track-Only', + '${pickedSource!.enforceTrackOnly ? 'Source' : 'App'} is Track-Only', items: const [], defaultValues: const [], message: @@ -222,7 +222,11 @@ class _AddAppPageState extends State { (pickedSource!.additionalSourceAppSpecificDefaults .isNotEmpty || pickedSource! - .additionalAppSpecificSourceAgnosticDefaults + .additionalAppSpecificSourceAgnosticFormItems + .where((e) => pickedSource!.enforceTrackOnly + ? e.key != 'trackOnlyFormItemKey' + : true) + .map((e) => [e]) .isNotEmpty)) Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -257,33 +261,27 @@ class _AddAppPageState extends State { }, defaultValues: pickedSource! .additionalSourceAppSpecificDefaults), - if (pickedSource! - .additionalSourceAppSpecificFormItems - .isNotEmpty) - const SizedBox( - height: 8, - ), - if (pickedSource! - .additionalAppSpecificSourceAgnosticFormItems - .isNotEmpty) - GeneratedForm( - items: pickedSource! - .additionalAppSpecificSourceAgnosticFormItems - .map((e) => [e]) - .toList(), - onValueChanges: (values, valid, isBuilding) { - if (isBuilding) { + GeneratedForm( + items: pickedSource! + .additionalAppSpecificSourceAgnosticFormItems + .where((e) => pickedSource!.enforceTrackOnly + ? e.key != 'trackOnlyFormItemKey' + : true) + .map((e) => [e]) + .toList(), + onValueChanges: (values, valid, isBuilding) { + if (isBuilding) { + otherAdditionalData = values; + otherAdditionalDataIsValid = valid; + } else { + setState(() { otherAdditionalData = values; otherAdditionalDataIsValid = valid; - } else { - setState(() { - otherAdditionalData = values; - otherAdditionalDataIsValid = valid; - }); - } - }, - defaultValues: pickedSource! - .additionalAppSpecificSourceAgnosticDefaults), + }); + } + }, + defaultValues: pickedSource! + .additionalAppSpecificSourceAgnosticDefaults), if (pickedSource! .additionalAppSpecificSourceAgnosticDefaults .isNotEmpty) @@ -304,16 +302,15 @@ class _AddAppPageState extends State { const SizedBox( height: 8, ), - ...sourceProvider - .getSourceHosts() + ...sourceProvider.sources .map((e) => GestureDetector( onTap: () { - launchUrlString('https://$e', + launchUrlString('https://${e.host}', mode: LaunchMode.externalApplication); }, child: Text( - e, + '${e.runtimeType.toString()}${e.enforceTrackOnly ? ' (Track-Only)' : ''}', style: const TextStyle( decoration: TextDecoration.underline, diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 5e9e693..bc2f4ba 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:html/dom.dart'; import 'package:http/http.dart'; +import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/gitlab.dart'; @@ -209,7 +210,8 @@ class SourceProvider { IzzyOnDroid(), Mullvad(), Signal(), - SourceForge() + SourceForge(), + APKMirror() ]; // Add more mass url source classes here so they are available via the service @@ -254,7 +256,7 @@ class SourceProvider { return false; } } - return getSourceHosts().contains(parts.last); + return sources.map((e) => e.host).contains(parts.last); } Future getApp(AppSource source, String url, List additionalData, @@ -306,6 +308,4 @@ class SourceProvider { } return [apps, errors]; } - - List getSourceHosts() => sources.map((e) => e.host).toList(); }