// Defines App sources and provides functions used to interact with them // AppSource is an abstract class with a concrete implementation for each source import 'dart:convert'; import 'package:html/dom.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/mullvad.dart'; import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart'; class AppNames { late String author; late String name; AppNames(this.author, this.name); } class APKDetails { late String version; late List apkUrls; APKDetails(this.version, this.apkUrls); } class App { late String id; late String url; late String author; late String name; String? installedVersion; late String latestVersion; List apkUrls = []; late int preferredApkIndex; late List additionalData; App( this.id, this.url, this.author, this.name, this.installedVersion, this.latestVersion, this.apkUrls, this.preferredApkIndex, this.additionalData); @override String toString() { return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls'; } factory App.fromJson(Map json) => App( json['id'] as String, json['url'] as String, json['author'] as String, json['name'] as String, json['installedVersion'] == null ? null : json['installedVersion'] as String, json['latestVersion'] as String, json['apkUrls'] == null ? [] : List.from(jsonDecode(json['apkUrls'])), json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int, json['additionalData'] == null ? SourceProvider().getSource(json['url']).additionalDataDefaults : List.from(jsonDecode(json['additionalData']))); Map toJson() => { 'id': id, 'url': url, 'author': author, 'name': name, 'installedVersion': installedVersion, 'latestVersion': latestVersion, 'apkUrls': jsonEncode(apkUrls), 'preferredApkIndex': preferredApkIndex, 'additionalData': jsonEncode(additionalData) }; } escapeRegEx(String s) { return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) { return '\\${x[0]}'; }); } makeUrlHttps(String url) { if (url.toLowerCase().indexOf('http://') != 0 && url.toLowerCase().indexOf('https://') != 0) { url = 'https://$url'; } if (url.toLowerCase().indexOf('https://www.') == 0) { url = 'https://${url.substring(12)}'; } return url; } const String couldNotFindReleases = 'Could not find a suitable release'; const String couldNotFindLatestVersion = 'Could not determine latest release version'; String notValidURL(String sourceName) { return 'Not a valid $sourceName App URL'; } const String noAPKFound = 'No APK found'; List getLinksFromParsedHTML( Document dom, RegExp hrefPattern, String prependToLinks) => dom .querySelectorAll('a') .where((element) { if (element.attributes['href'] == null) return false; return hrefPattern.hasMatch(element.attributes['href']!); }) .map((e) => '$prependToLinks${e.attributes['href']!}') .toList(); abstract class AppSource { late String host; String standardizeURL(String url); Future getLatestAPKDetails( String standardUrl, List additionalData); AppNames getAppNames(String standardUrl); late List> additionalDataFormItems; late List additionalDataDefaults; } abstract class MassAppSource { late String name; late List requiredArgs; Future> getUrls(List args); } class SourceProvider { // Add more source classes here so they are available via the service List sources = [ GitHub(), GitLab(), FDroid(), IzzyOnDroid(), Mullvad(), Signal(), SourceForge() ]; // Add more mass source classes here so they are available via the service List massSources = [GitHubStars()]; AppSource getSource(String url) { url = makeUrlHttps(url); AppSource? source; for (var s in sources) { if (url.toLowerCase().contains('://${s.host}')) { source = s; break; } } if (source == null) { throw 'URL does not match a known source'; } return source; } bool doesSourceHaveRequiredAdditionalData(AppSource source) { for (var row in source.additionalDataFormItems) { for (var element in row) { if (element.required) { return true; } } } return false; } Future getApp(AppSource source, String url, List additionalData, {String customName = ''}) async { String standardUrl = source.standardizeURL(makeUrlHttps(url)); AppNames names = source.getAppNames(standardUrl); APKDetails apk = await source.getLatestAPKDetails(standardUrl, additionalData); return App( '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}', standardUrl, names.author[0].toUpperCase() + names.author.substring(1), customName.trim().isNotEmpty ? customName : names.name[0].toUpperCase() + names.name.substring(1), null, apk.version, apk.apkUrls, apk.apkUrls.length - 1, additionalData); } /// Returns a length 2 list, where the first element is a list of Apps and /// the second is a Map of URLs and errors Future> getApps(List urls) async { List apps = []; Map errors = {}; for (var url in urls) { try { var source = getSource(url); apps.add(await getApp(source, url, source.additionalDataDefaults)); } catch (e) { errors.addAll({url: e}); } } return [apps, errors]; } List getSourceHosts() => sources.map((e) => e.host).toList(); }