diff --git a/lib/main.dart b/lib/main.dart index 7565aae..72e7af4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:obtainium/services/apk_service.dart'; +import 'package:obtainium/services/source_service.dart'; import 'package:provider/provider.dart'; void main() async { @@ -11,6 +12,7 @@ void main() async { create: (context) => APKService(), dispose: (context, apkInstallService) => apkInstallService.dispose(), ), + Provider(create: (context) => SourceService()) ], child: const MyApp(), )); @@ -81,15 +83,17 @@ class _MyHomePageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () { - var names = getAppNamesFromGitHubURL(urls[ind]); - if (names != null) { - Provider.of(context, listen: false) - .downloadAndInstallAPK( - urls[ind], "${names["author"]!}_${names["appName"]!}"); - setState(() { - ind = ind == (urls.length - 1) ? 0 : ind + 1; - }); - } + var source = Provider.of(context).getSource(urls[ind]); + + var standardURL = Provider.of(context) + .standardizeURL(urls[ind], source.standardURLRegEx); + var names = + Provider.of(context).getAppNames(standardURL); + Provider.of(context, listen: false).downloadAndInstallAPK( + urls[ind], "${names.author}_${names.name}"); + setState(() { + ind = ind == (urls.length - 1) ? 0 : ind + 1; + }); }, tooltip: 'Increment', child: const Icon(Icons.add), diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart index d73f8ec..5fd2171 100644 --- a/lib/services/source_service.dart +++ b/lib/services/source_service.dart @@ -1,5 +1,123 @@ +import 'dart:convert'; +import 'package:http/http.dart'; +import 'package:markdown/markdown.dart'; +import 'package:html/parser.dart'; + +// Sub-classes of App Source + +class AppNames { + late String author; + late String name; + + AppNames(this.author, this.name); +} + +class APKDetails { + late String version; + late String downloadUrl; + + APKDetails(this.version, this.downloadUrl); +} + +// App Source abstract class (GitHub, GitLab, etc.) + +abstract class AppSource { + late RegExp standardURLRegEx; + Future getLatestAPKUrl(String url); + Future getReadMeHTML(String url); + Future getBase64IconURLFromHTML(String url, String html); + + AppSource(this.standardURLRegEx); +} + +// Specific App Source definitions + +class GitHub extends AppSource { + GitHub() : super(RegExp(r"^https?://github.com/[^/]*/[^/]*")); + + String getRawContentURL(String url) { + int tempInd1 = url.indexOf('://') + 3; + int tempInd2 = url.indexOf('://') + 13; + return "${url.substring(0, tempInd1)}raw.githubusercontent.com${url.substring(tempInd2)}"; + } + + @override + Future getLatestAPKUrl(String url) async { + int tempInd = url.indexOf('://') + 3; + Response res = await get(Uri.parse( + "${url.substring(0, tempInd)}api.${url.substring(tempInd)}/releases/latest")); + if (res.statusCode == 200) { + var release = jsonDecode(res.body); + for (var i = 0; i < release['assets'].length; i++) { + if (release['assets'][i] + .name + .toString() + .toLowerCase() + .endsWith(".apk")) { + return APKDetails(release['tag_name'], + release['assets'][i]['browser_download_url']); + } + } + throw "No APK found"; + } else { + throw "Unable to fetch release info"; + } + } + + @override + Future getReadMeHTML(String url) async { + String uri = getRawContentURL(url); + List possibleSuffixes = ["main/README.md", "master/README.md"]; + for (var i = 0; i < possibleSuffixes.length; i++) { + Response res = await get(Uri.parse("$uri/${possibleSuffixes[i]}")); + if (res.statusCode == 200) { + return markdownToHtml(res.body); + } + } + return null; + } + + @override + Future getBase64IconURLFromHTML(String url, String html) async { + var icon = parse(html).getElementsByClassName("img")?[0]; + if (icon != null) { + String uri = getRawContentURL(url); + List possibleBranches = ["main", "master"]; + for (var i = 0; i < possibleBranches.length; i++) { + var imgUrl = "$uri/${possibleBranches[i]}/${icon.attributes['src']}"; + Response res = await get(Uri.parse(imgUrl)); + if (res.statusCode == 200) { + return imgUrl; + } + } + } + return null; + } +} + class SourceService { - SourceService(); + String standardizeURL(String url, RegExp standardURLRegEx) { + var match = standardURLRegEx.firstMatch(url.toLowerCase()); + if (match == null) { + throw "Not a valid URL"; + } + return url.substring(0, match.end); + } + + AppNames getAppNames(String standardURL) { + String temp = standardURL.substring(standardURL.indexOf('://') + 3); + List names = temp.substring(temp.indexOf('/')).split('/'); + return AppNames(names[0], names[1]); + } + + // Add more source classes here so they are available via the service + var github = GitHub(); + AppSource getSource(String url) { + if (url.toLowerCase().contains('://github.com')) { + return github; + } + throw "URL does not match a known source"; + } } /* @@ -7,7 +125,7 @@ class SourceService { - Make a function that gets the App title and Author name from a github URL, do the same for gitlab (can't fail) - Make a function that takes a github URL and finds the latest APK release if any (with version), do the same for gitlab (fail = error) - Make a function that takes a github URL and returns a README HTML if any, do the same for gitlab (fail = "no description") -- Make a function that looks for the first image in a README HTML and returns a small base64 encoded version of it (fail = generic icon) +- Make a function that looks for the first image in a README HTML and returns its url (fail = no icon) - Make a function that integrates all above and returns an App object for a given github URL, do the same for gitlab diff --git a/pubspec.lock b/pubspec.lock index 1965bb6..0f70400 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -130,6 +137,27 @@ packages: description: flutter source: sdk version: "0.0.0" + html: + dependency: "direct main" + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.5" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" lints: dependency: transitive description: @@ -137,6 +165,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + markdown: + dependency: "direct main" + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" matcher: dependency: transitive description: @@ -310,6 +345,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.8.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ef18a3b..eb660fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,9 @@ dependencies: flutter_fgbg: ^0.2.0 flutter_local_notifications: ^9.7.0 provider: ^6.0.3 + http: ^0.13.5 + markdown: ^6.0.0 + html: ^0.15.0 dev_dependencies: flutter_test: