mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-04 07:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			28 Commits
		
	
	
		
			v0.11.9-be
			...
			v0.11.15-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c7cd35b6a1 | ||
| 
						 | 
					a8a3fce33a | ||
| 
						 | 
					3a38cedcf5 | ||
| 
						 | 
					69ccefcf1a | ||
| 
						 | 
					d3932f317d | ||
| 
						 | 
					895deeead5 | ||
| 
						 | 
					4c04af3868 | ||
| 
						 | 
					07c490bb0e | ||
| 
						 | 
					a081d553bb | ||
| 
						 | 
					3bc5837999 | ||
| 
						 | 
					9fbe524818 | ||
| 
						 | 
					c21a9d7292 | ||
| 
						 | 
					9c6068b270 | ||
| 
						 | 
					cd86d6112b | ||
| 
						 | 
					1112c79c14 | ||
| 
						 | 
					08555bac75 | ||
| 
						 | 
					6db31e2b24 | ||
| 
						 | 
					48d2532323 | ||
| 
						 | 
					f1fc43a3e7 | ||
| 
						 | 
					280827d8ec | ||
| 
						 | 
					05ee0f9c48 | ||
| 
						 | 
					ef06ae289e | ||
| 
						 | 
					bd0e322465 | ||
| 
						 | 
					a93a2411fa | ||
| 
						 | 
					26e6eef72e | ||
| 
						 | 
					e49a6e311b | ||
| 
						 | 
					53d3397651 | ||
| 
						 | 
					fe540f5e61 | 
@@ -19,6 +19,9 @@ Currently supported App sources:
 | 
			
		||||
- Third Party F-Droid Repos
 | 
			
		||||
  - Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
 | 
			
		||||
- [Steam](https://store.steampowered.com/mobile)
 | 
			
		||||
- [Telegram App](https://telegram.org)
 | 
			
		||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
 | 
			
		||||
- [Neutron Code](https://neutroncode.com)
 | 
			
		||||
- "HTML" (Fallback)
 | 
			
		||||
  - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								assets/ca/lets-encrypt-r3.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								assets/ca/lets-encrypt-r3.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
 | 
			
		||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
 | 
			
		||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
 | 
			
		||||
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
 | 
			
		||||
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
 | 
			
		||||
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
 | 
			
		||||
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
 | 
			
		||||
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
 | 
			
		||||
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
 | 
			
		||||
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
 | 
			
		||||
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
 | 
			
		||||
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
 | 
			
		||||
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
 | 
			
		||||
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
 | 
			
		||||
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
 | 
			
		||||
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
 | 
			
		||||
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
 | 
			
		||||
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
 | 
			
		||||
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
 | 
			
		||||
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
 | 
			
		||||
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
 | 
			
		||||
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
 | 
			
		||||
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
 | 
			
		||||
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
 | 
			
		||||
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
 | 
			
		||||
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
 | 
			
		||||
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
 | 
			
		||||
nLRbwHOoq7hHwg==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
@@ -217,9 +217,9 @@
 | 
			
		||||
    "releaseDateAsVersionExplanation": "Questa opzione dovrebbe essere usata solo per le App in cui il rilevamento della versione non funziona correttamente, ma è disponibile una data di rilascio.",
 | 
			
		||||
    "changes": "Novità",
 | 
			
		||||
    "releaseDate": "Data di rilascio",
 | 
			
		||||
    "importFromURLsInFile": "Import from URLs in File (like OPML)",
 | 
			
		||||
    "versionDetection": "Version Detection",
 | 
			
		||||
    "standardVersionDetection": "Standard version detection",
 | 
			
		||||
    "importFromURLsInFile": "Importa da URL in file (come OPML)",
 | 
			
		||||
    "versionDetection": "Rilevamento di versione",
 | 
			
		||||
    "standardVersionDetection": "Rilevamento di versione standard",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Rimuovere l'App?",
 | 
			
		||||
        "other": "Rimuovere le App?"
 | 
			
		||||
 
 | 
			
		||||
@@ -118,9 +118,11 @@ class Codeberg extends AppSource {
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      var changeLog = targetRelease['body'].toString();
 | 
			
		||||
      return APKDetails(version, targetRelease['apkUrls'] as List<String>,
 | 
			
		||||
          getAppNames(standardUrl),
 | 
			
		||||
          releaseDate: releaseDate);
 | 
			
		||||
          releaseDate: releaseDate,
 | 
			
		||||
          changeLog: changeLog.isEmpty ? null : changeLog);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,6 @@ class FDroid extends AppSource {
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? tryInferringAppId(String standardUrl,
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const {}}) {
 | 
			
		||||
 
 | 
			
		||||
@@ -160,9 +160,11 @@ class GitHub extends AppSource {
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      var changeLog = targetRelease['body'].toString();
 | 
			
		||||
      return APKDetails(version, targetRelease['apkUrls'] as List<String>,
 | 
			
		||||
          getAppNames(standardUrl),
 | 
			
		||||
          releaseDate: releaseDate);
 | 
			
		||||
          releaseDate: releaseDate,
 | 
			
		||||
          changeLog: changeLog.isEmpty ? null : changeLog);
 | 
			
		||||
    } else {
 | 
			
		||||
      rateLimitErrorCheck(res);
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,6 @@ class HTML extends AppSource {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,6 @@ class IzzyOnDroid extends AppSource {
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? tryInferringAppId(String standardUrl,
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const {}}) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								lib/app_sources/neutroncode.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								lib/app_sources/neutroncode.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class NeutronCode extends AppSource {
 | 
			
		||||
  NeutronCode() {
 | 
			
		||||
    host = 'neutroncode.com';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
    }
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
 | 
			
		||||
 | 
			
		||||
  String monthNameToNumberString(String s) {
 | 
			
		||||
    switch (s.toLowerCase()) {
 | 
			
		||||
      case 'january':
 | 
			
		||||
        return '01';
 | 
			
		||||
      case 'february':
 | 
			
		||||
        return '02';
 | 
			
		||||
      case 'march':
 | 
			
		||||
        return '03';
 | 
			
		||||
      case 'april':
 | 
			
		||||
        return '04';
 | 
			
		||||
      case 'may':
 | 
			
		||||
        return '05';
 | 
			
		||||
      case 'june':
 | 
			
		||||
        return '06';
 | 
			
		||||
      case 'july':
 | 
			
		||||
        return '07';
 | 
			
		||||
      case 'august':
 | 
			
		||||
        return '08';
 | 
			
		||||
      case 'september':
 | 
			
		||||
        return '09';
 | 
			
		||||
      case 'october':
 | 
			
		||||
        return '10';
 | 
			
		||||
      case 'november':
 | 
			
		||||
        return '11';
 | 
			
		||||
      case 'december':
 | 
			
		||||
        return '12';
 | 
			
		||||
      default:
 | 
			
		||||
        throw ArgumentError('Invalid month name: $s');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  customDateParse(String dateString) {
 | 
			
		||||
    List<String> parts = dateString.split(' ');
 | 
			
		||||
    if (parts.length != 3) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    String result = '';
 | 
			
		||||
    for (var s in parts.reversed) {
 | 
			
		||||
      try {
 | 
			
		||||
        try {
 | 
			
		||||
          int.parse(s);
 | 
			
		||||
          result += '$s-';
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          result += '${monthNameToNumberString(s)}-';
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result.substring(0, result.length - 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await get(Uri.parse(standardUrl));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var http = parse(res.body);
 | 
			
		||||
      var name = http.querySelector('.pd-title')?.innerHtml;
 | 
			
		||||
      var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
 | 
			
		||||
      if (filename == null) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      }
 | 
			
		||||
      var version =
 | 
			
		||||
          http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml;
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      String? apkUrl = 'https://$host/download/$filename';
 | 
			
		||||
      var dateStringOriginal =
 | 
			
		||||
          http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
 | 
			
		||||
      var dateString = dateStringOriginal != null
 | 
			
		||||
          ? (customDateParse(dateStringOriginal))
 | 
			
		||||
          : null;
 | 
			
		||||
      var changeLogElements = http.querySelectorAll('.pd-fdesc p');
 | 
			
		||||
      return APKDetails(version, [apkUrl],
 | 
			
		||||
          AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
 | 
			
		||||
          releaseDate: dateString != null ? DateTime.parse(dateString) : null,
 | 
			
		||||
          changeLog: changeLogElements.isNotEmpty
 | 
			
		||||
              ? changeLogElements.last.innerHtml
 | 
			
		||||
              : null);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,9 +13,6 @@ class Signal extends AppSource {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,6 @@ class SourceForge extends AppSource {
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,6 @@ class SteamMobile extends AppSource {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								lib/app_sources/telegramapp.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								lib/app_sources/telegramapp.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
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 TelegramApp extends AppSource {
 | 
			
		||||
  TelegramApp() {
 | 
			
		||||
    host = 'telegram.org';
 | 
			
		||||
    name = 'Telegram ${tr('app')}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var http = parse(res.body);
 | 
			
		||||
      var messages =
 | 
			
		||||
          http.querySelectorAll('.tgme_widget_message_text.js-message_text');
 | 
			
		||||
      var version = messages.isNotEmpty
 | 
			
		||||
          ? messages.last.innerHtml.split('\n').first.trim().split(' ').first
 | 
			
		||||
          : null;
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      String? apkUrl = 'https://telegram.org/dl/android/apk';
 | 
			
		||||
      return APKDetails(version, [apkUrl], AppNames('Telegram', 'Telegram'));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								lib/app_sources/vlc.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								lib/app_sources/vlc.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class VLC extends AppSource {
 | 
			
		||||
  VLC() {
 | 
			
		||||
    host = 'videolan.org';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await get(
 | 
			
		||||
        Uri.parse('https://www.videolan.org/vlc/download-android.html'));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var dwUrlBase = 'get.videolan.org/vlc-android';
 | 
			
		||||
      var dwLinks = parse(res.body)
 | 
			
		||||
          .querySelectorAll('a')
 | 
			
		||||
          .where((element) =>
 | 
			
		||||
              element.attributes['href']?.contains(dwUrlBase) ?? false)
 | 
			
		||||
          .toList();
 | 
			
		||||
      String? version = dwLinks.isNotEmpty
 | 
			
		||||
          ? dwLinks.first.attributes['href']
 | 
			
		||||
              ?.split('/')
 | 
			
		||||
              .where((s) => s.isNotEmpty)
 | 
			
		||||
              .last
 | 
			
		||||
          : null;
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      String? targetUrl = 'https://$dwUrlBase/$version/';
 | 
			
		||||
      Response res2 = await get(Uri.parse(targetUrl));
 | 
			
		||||
      String mirrorDwBase =
 | 
			
		||||
          'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
 | 
			
		||||
      List<String> apkUrls = [];
 | 
			
		||||
      if (res2.statusCode == 200) {
 | 
			
		||||
        apkUrls = parse(res2.body)
 | 
			
		||||
            .querySelectorAll('a')
 | 
			
		||||
            .map((e) => e.attributes['href'])
 | 
			
		||||
            .where((h) =>
 | 
			
		||||
                h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk'))
 | 
			
		||||
            .map((e) => mirrorDwBase + e!)
 | 
			
		||||
            .toList();
 | 
			
		||||
      } else {
 | 
			
		||||
        throw getObtainiumHttpError(res2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return APKDetails(version, apkUrls, AppNames('VideoLAN', 'VLC'));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								lib/app_sources/whatsapp.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/app_sources/whatsapp.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class WhatsApp extends AppSource {
 | 
			
		||||
  WhatsApp() {
 | 
			
		||||
    host = 'whatsapp.com';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String standardizeURL(String url) {
 | 
			
		||||
    return 'https://$host';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String> apkUrlPrefetchModifier(String apkUrl) async {
 | 
			
		||||
    Response res = await get(Uri.parse('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)
 | 
			
		||||
          .where((e) =>
 | 
			
		||||
              e!.contains('scontent.whatsapp.net') &&
 | 
			
		||||
              e.contains('WhatsApp.apk'))
 | 
			
		||||
          .toList();
 | 
			
		||||
      if (targetLinks.isEmpty) {
 | 
			
		||||
        throw NoAPKError();
 | 
			
		||||
      }
 | 
			
		||||
      return targetLinks[0]!;
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var targetElements = parse(res.body)
 | 
			
		||||
          .querySelectorAll('p')
 | 
			
		||||
          .where((element) => element.innerHtml.contains('Version '))
 | 
			
		||||
          .toList();
 | 
			
		||||
      if (targetElements.isEmpty) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      var vLines = targetElements[0]
 | 
			
		||||
          .innerHtml
 | 
			
		||||
          .split('\n')
 | 
			
		||||
          .where((element) => element.contains('Version '))
 | 
			
		||||
          .toList();
 | 
			
		||||
      if (vLines.isEmpty) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(vLines[0]);
 | 
			
		||||
      if (versionMatch == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      String version =
 | 
			
		||||
          vLines[0].substring(versionMatch.start, versionMatch.end);
 | 
			
		||||
      return APKDetails(
 | 
			
		||||
          version,
 | 
			
		||||
          [
 | 
			
		||||
            'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
 | 
			
		||||
          ],
 | 
			
		||||
          AppNames('Meta', 'WhatsApp'));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -476,6 +476,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
        rowItems.add(Expanded(
 | 
			
		||||
            child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                children: [
 | 
			
		||||
              rowInput.value,
 | 
			
		||||
              ...widget.items[rowInputs.key][rowInput.key].belowWidgets
 | 
			
		||||
 
 | 
			
		||||
@@ -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.11.9';
 | 
			
		||||
const String currentVersion = '0.11.15';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
@@ -147,6 +147,14 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
 | 
			
		||||
void main() async {
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  try {
 | 
			
		||||
    ByteData data =
 | 
			
		||||
        await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
 | 
			
		||||
    SecurityContext.defaultContext
 | 
			
		||||
        .setTrustedCertificatesBytes(data.buffer.asUint8List());
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    // Already added, do nothing (see #375)
 | 
			
		||||
  }
 | 
			
		||||
  await EasyLocalization.ensureInitialized();
 | 
			
		||||
  if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
 | 
			
		||||
    SystemChrome.setSystemUIOverlayStyle(
 | 
			
		||||
@@ -210,7 +218,7 @@ class _ObtainiumState extends State<Obtainium> {
 | 
			
		||||
              {'includePrereleases': true},
 | 
			
		||||
              null,
 | 
			
		||||
              false)
 | 
			
		||||
        ]);
 | 
			
		||||
        ], onlyIfExists: false);
 | 
			
		||||
      }
 | 
			
		||||
      if (!supportedLocales
 | 
			
		||||
              .map((e) => e.languageCode)
 | 
			
		||||
 
 | 
			
		||||
@@ -149,14 +149,14 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
            app.installedVersion = app.latestVersion;
 | 
			
		||||
          }
 | 
			
		||||
          app.categories = pickedCategories;
 | 
			
		||||
          await appsProvider.saveApps([app]);
 | 
			
		||||
          await appsProvider.saveApps([app], onlyIfExists: false);
 | 
			
		||||
 | 
			
		||||
          return app;
 | 
			
		||||
        }
 | 
			
		||||
      }()
 | 
			
		||||
          .then((app) {
 | 
			
		||||
        if (app != null) {
 | 
			
		||||
          Navigator.push(context,
 | 
			
		||||
          Navigator.push(globalNavigatorKey.currentContext ?? context,
 | 
			
		||||
              MaterialPageRoute(builder: (context) => AppPage(appId: app.id)));
 | 
			
		||||
        }
 | 
			
		||||
      }).catchError((e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:flutter_markdown/flutter_markdown.dart';
 | 
			
		||||
import 'package:obtainium/components/custom_app_bar.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
@@ -14,6 +15,7 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:share_plus/share_plus.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
import 'package:markdown/markdown.dart' as md;
 | 
			
		||||
 | 
			
		||||
class AppsPage extends StatefulWidget {
 | 
			
		||||
  const AppsPage({super.key});
 | 
			
		||||
@@ -229,9 +231,88 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
            SliverList(
 | 
			
		||||
                delegate: SliverChildBuilderDelegate(
 | 
			
		||||
                    (BuildContext context, int index) {
 | 
			
		||||
              String? changesUrl = SourceProvider()
 | 
			
		||||
                  .getSource(listedApps[index].app.url)
 | 
			
		||||
              AppSource appSource =
 | 
			
		||||
                  SourceProvider().getSource(listedApps[index].app.url);
 | 
			
		||||
              String? changesUrl = appSource
 | 
			
		||||
                  .changeLogPageFromStandardUrl(listedApps[index].app.url);
 | 
			
		||||
              String? changeLog = listedApps[index].app.changeLog;
 | 
			
		||||
              var showChanges = (changeLog == null && changesUrl == null)
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : () {
 | 
			
		||||
                      if (changeLog != null) {
 | 
			
		||||
                        showDialog(
 | 
			
		||||
                            context: context,
 | 
			
		||||
                            builder: (BuildContext context) {
 | 
			
		||||
                              return GeneratedFormModal(
 | 
			
		||||
                                title: tr('changes'),
 | 
			
		||||
                                items: const [],
 | 
			
		||||
                                additionalWidgets: [
 | 
			
		||||
                                  changesUrl != null
 | 
			
		||||
                                      ? GestureDetector(
 | 
			
		||||
                                          child: Text(
 | 
			
		||||
                                            changesUrl,
 | 
			
		||||
                                            style: const TextStyle(
 | 
			
		||||
                                                decoration:
 | 
			
		||||
                                                    TextDecoration.underline,
 | 
			
		||||
                                                fontStyle: FontStyle.italic),
 | 
			
		||||
                                          ),
 | 
			
		||||
                                          onTap: () {
 | 
			
		||||
                                            launchUrlString(changesUrl,
 | 
			
		||||
                                                mode: LaunchMode
 | 
			
		||||
                                                    .externalApplication);
 | 
			
		||||
                                          },
 | 
			
		||||
                                        )
 | 
			
		||||
                                      : const SizedBox.shrink(),
 | 
			
		||||
                                  changesUrl != null
 | 
			
		||||
                                      ? const SizedBox(
 | 
			
		||||
                                          height: 16,
 | 
			
		||||
                                        )
 | 
			
		||||
                                      : const SizedBox.shrink(),
 | 
			
		||||
                                  appSource.changeLogIfAnyIsMarkDown
 | 
			
		||||
                                      ? SizedBox(
 | 
			
		||||
                                          width:
 | 
			
		||||
                                              MediaQuery.of(context).size.width,
 | 
			
		||||
                                          height: MediaQuery.of(context)
 | 
			
		||||
                                                  .size
 | 
			
		||||
                                                  .height -
 | 
			
		||||
                                              350,
 | 
			
		||||
                                          child: Markdown(
 | 
			
		||||
                                            data: changeLog,
 | 
			
		||||
                                            onTapLink: (text, href, title) {
 | 
			
		||||
                                              if (href != null) {
 | 
			
		||||
                                                launchUrlString(
 | 
			
		||||
                                                    href.startsWith(
 | 
			
		||||
                                                                'http://') ||
 | 
			
		||||
                                                            href.startsWith(
 | 
			
		||||
                                                                'https://')
 | 
			
		||||
                                                        ? href
 | 
			
		||||
                                                        : '${Uri.parse(listedApps[index].app.url).origin}/$href',
 | 
			
		||||
                                                    mode: LaunchMode
 | 
			
		||||
                                                        .externalApplication);
 | 
			
		||||
                                              }
 | 
			
		||||
                                            },
 | 
			
		||||
                                            extensionSet: md.ExtensionSet(
 | 
			
		||||
                                              md.ExtensionSet.gitHubFlavored
 | 
			
		||||
                                                  .blockSyntaxes,
 | 
			
		||||
                                              [
 | 
			
		||||
                                                md.EmojiSyntax(),
 | 
			
		||||
                                                ...md
 | 
			
		||||
                                                    .ExtensionSet
 | 
			
		||||
                                                    .gitHubFlavored
 | 
			
		||||
                                                    .inlineSyntaxes
 | 
			
		||||
                                              ],
 | 
			
		||||
                                            ),
 | 
			
		||||
                                          ))
 | 
			
		||||
                                      : Text(changeLog),
 | 
			
		||||
                                ],
 | 
			
		||||
                                singleNullReturnButton: tr('ok'),
 | 
			
		||||
                              );
 | 
			
		||||
                            });
 | 
			
		||||
                      } else {
 | 
			
		||||
                        launchUrlString(changesUrl!,
 | 
			
		||||
                            mode: LaunchMode.externalApplication);
 | 
			
		||||
                      }
 | 
			
		||||
                    };
 | 
			
		||||
              var transparent = const Color.fromARGB(0, 0, 0, 0).value;
 | 
			
		||||
              var hasUpdate = listedApps[index].app.installedVersion != null &&
 | 
			
		||||
                  listedApps[index].app.installedVersion !=
 | 
			
		||||
@@ -366,25 +447,22 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      GestureDetector(
 | 
			
		||||
                                          onTap: changesUrl == null
 | 
			
		||||
                                              ? null
 | 
			
		||||
                                              : () {
 | 
			
		||||
                                                  launchUrlString(changesUrl,
 | 
			
		||||
                                                      mode: LaunchMode
 | 
			
		||||
                                                          .externalApplication);
 | 
			
		||||
                                                },
 | 
			
		||||
                                          onTap: showChanges,
 | 
			
		||||
                                          child: Text(
 | 
			
		||||
                                            listedApps[index].app.releaseDate ==
 | 
			
		||||
                                                    null
 | 
			
		||||
                                                ? tr('changes')
 | 
			
		||||
                                                ? showChanges != null
 | 
			
		||||
                                                    ? tr('changes')
 | 
			
		||||
                                                    : ''
 | 
			
		||||
                                                : DateFormat('yyyy-MM-dd')
 | 
			
		||||
                                                    .format(listedApps[index]
 | 
			
		||||
                                                        .app
 | 
			
		||||
                                                        .releaseDate!),
 | 
			
		||||
                                            style: const TextStyle(
 | 
			
		||||
                                            style: TextStyle(
 | 
			
		||||
                                                fontStyle: FontStyle.italic,
 | 
			
		||||
                                                decoration:
 | 
			
		||||
                                                    TextDecoration.underline),
 | 
			
		||||
                                                decoration: showChanges != null
 | 
			
		||||
                                                    ? TextDecoration.underline
 | 
			
		||||
                                                    : TextDecoration.none),
 | 
			
		||||
                                          ))
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,9 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
 | 
			
		||||
    NotificationsProvider? notificationsProvider =
 | 
			
		||||
        context?.read<NotificationsProvider>();
 | 
			
		||||
    var notifId = DownloadNotification(app.name, 0).id;
 | 
			
		||||
    if (apps[app.id] != null) {
 | 
			
		||||
      apps[app.id]!.downloadProgress = 0;
 | 
			
		||||
      notifyListeners();
 | 
			
		||||
@@ -155,8 +158,6 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      String downloadUrl = await SourceProvider()
 | 
			
		||||
          .getSource(app.url)
 | 
			
		||||
          .apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]);
 | 
			
		||||
      NotificationsProvider? notificationsProvider =
 | 
			
		||||
          context?.read<NotificationsProvider>();
 | 
			
		||||
      var notif = DownloadNotification(app.name, 100);
 | 
			
		||||
      notificationsProvider?.cancel(notif.id);
 | 
			
		||||
      int? prevProg;
 | 
			
		||||
@@ -173,7 +174,6 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        }
 | 
			
		||||
        prevProg = prog;
 | 
			
		||||
      });
 | 
			
		||||
      notificationsProvider?.cancel(notif.id);
 | 
			
		||||
      // Delete older versions of the APK if any
 | 
			
		||||
      for (var file in downloadedFile.parent.listSync()) {
 | 
			
		||||
        var fn = file.path.split('/').last;
 | 
			
		||||
@@ -201,6 +201,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      }
 | 
			
		||||
      return DownloadedApk(app.id, downloadedFile);
 | 
			
		||||
    } finally {
 | 
			
		||||
      notificationsProvider?.cancel(notifId);
 | 
			
		||||
      if (apps[app.id] != null) {
 | 
			
		||||
        apps[app.id]!.downloadProgress = null;
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
@@ -570,7 +571,21 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    List<App> newApps = (await getAppsDir())
 | 
			
		||||
        .listSync()
 | 
			
		||||
        .where((item) => item.path.toLowerCase().endsWith('.json'))
 | 
			
		||||
        .map((e) => App.fromJson(jsonDecode(File(e.path).readAsStringSync())))
 | 
			
		||||
        .map((e) {
 | 
			
		||||
          try {
 | 
			
		||||
            return App.fromJson(jsonDecode(File(e.path).readAsStringSync()));
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            if (err is FormatException) {
 | 
			
		||||
              logs.add('Corrupt JSON when loading App (will be ignored): $e');
 | 
			
		||||
              e.renameSync('${e.path}.corrupt');
 | 
			
		||||
              return App(
 | 
			
		||||
                  '', '', '', '', '', '', [], 0, {}, DateTime.now(), false);
 | 
			
		||||
            } else {
 | 
			
		||||
              rethrow;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .where((element) => element.id.isNotEmpty)
 | 
			
		||||
        .toList();
 | 
			
		||||
    var idsToDelete = apps.values
 | 
			
		||||
        .map((e) => e.app.id)
 | 
			
		||||
@@ -613,7 +628,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> saveApps(List<App> apps,
 | 
			
		||||
      {bool attemptToCorrectInstallStatus = true}) async {
 | 
			
		||||
      {bool attemptToCorrectInstallStatus = true,
 | 
			
		||||
      bool onlyIfExists = true}) async {
 | 
			
		||||
    attemptToCorrectInstallStatus =
 | 
			
		||||
        attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
 | 
			
		||||
    for (var app in apps) {
 | 
			
		||||
@@ -624,9 +640,15 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      }
 | 
			
		||||
      File('${(await getAppsDir()).path}/${app.id}.json')
 | 
			
		||||
          .writeAsStringSync(jsonEncode(app.toJson()));
 | 
			
		||||
      this.apps.update(
 | 
			
		||||
          app.id, (value) => AppInMemory(app, value.downloadProgress, info),
 | 
			
		||||
          ifAbsent: () => AppInMemory(app, null, info));
 | 
			
		||||
      try {
 | 
			
		||||
        this.apps.update(
 | 
			
		||||
            app.id, (value) => AppInMemory(app, value.downloadProgress, info),
 | 
			
		||||
            ifAbsent: onlyIfExists ? null : () => AppInMemory(app, null, info));
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        if (e is! ArgumentError || e.name != 'key') {
 | 
			
		||||
          rethrow;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
@@ -647,8 +669,11 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async {
 | 
			
		||||
    var showUninstallOption =
 | 
			
		||||
        apps.where((a) => a.installedVersion != null).isNotEmpty;
 | 
			
		||||
    var showUninstallOption = apps
 | 
			
		||||
        .where((a) =>
 | 
			
		||||
            a.installedVersion != null &&
 | 
			
		||||
            a.additionalSettings['trackOnly'] != true)
 | 
			
		||||
        .isNotEmpty;
 | 
			
		||||
    var values = await showDialog(
 | 
			
		||||
        context: context,
 | 
			
		||||
        builder: (BuildContext ctx) {
 | 
			
		||||
@@ -809,7 +834,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        a.installedVersion = apps[a.id]?.app.installedVersion;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    await saveApps(importedApps);
 | 
			
		||||
    await saveApps(importedApps, onlyIfExists: false);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
    return importedApps.length;
 | 
			
		||||
  }
 | 
			
		||||
@@ -829,7 +854,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      if (apps.containsKey(app.id)) {
 | 
			
		||||
        errorsMap.addAll({app.id: tr('appAlreadyAdded')});
 | 
			
		||||
      } else {
 | 
			
		||||
        await saveApps([app]);
 | 
			
		||||
        await saveApps([app], onlyIfExists: false);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    List<List<String>> errors =
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,13 @@ import 'package:obtainium/app_sources/gitlab.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/izzyondroid.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
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/steammobile.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/telegramapp.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/vlc.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/whatsapp.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
 | 
			
		||||
@@ -34,8 +38,10 @@ class APKDetails {
 | 
			
		||||
  late List<String> apkUrls;
 | 
			
		||||
  late AppNames names;
 | 
			
		||||
  late DateTime? releaseDate;
 | 
			
		||||
  late String? changeLog;
 | 
			
		||||
 | 
			
		||||
  APKDetails(this.version, this.apkUrls, this.names, {this.releaseDate});
 | 
			
		||||
  APKDetails(this.version, this.apkUrls, this.names,
 | 
			
		||||
      {this.releaseDate, this.changeLog});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class App {
 | 
			
		||||
@@ -52,6 +58,7 @@ class App {
 | 
			
		||||
  bool pinned = false;
 | 
			
		||||
  List<String> categories;
 | 
			
		||||
  late DateTime? releaseDate;
 | 
			
		||||
  late String? changeLog;
 | 
			
		||||
  App(
 | 
			
		||||
      this.id,
 | 
			
		||||
      this.url,
 | 
			
		||||
@@ -65,7 +72,8 @@ class App {
 | 
			
		||||
      this.lastUpdateCheck,
 | 
			
		||||
      this.pinned,
 | 
			
		||||
      {this.categories = const [],
 | 
			
		||||
      this.releaseDate});
 | 
			
		||||
      this.releaseDate,
 | 
			
		||||
      this.changeLog});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
@@ -128,34 +136,35 @@ class App {
 | 
			
		||||
      preferredApkIndex = 0;
 | 
			
		||||
    }
 | 
			
		||||
    return 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<String>.from(jsonDecode(json['apkUrls'])),
 | 
			
		||||
      preferredApkIndex,
 | 
			
		||||
      additionalSettings,
 | 
			
		||||
      json['lastUpdateCheck'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
 | 
			
		||||
      json['pinned'] ?? false,
 | 
			
		||||
      categories: json['categories'] != null
 | 
			
		||||
          ? (json['categories'] as List<dynamic>)
 | 
			
		||||
              .map((e) => e.toString())
 | 
			
		||||
              .toList()
 | 
			
		||||
          : json['category'] != null
 | 
			
		||||
              ? [json['category'] as String]
 | 
			
		||||
              : [],
 | 
			
		||||
      releaseDate: json['releaseDate'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
 | 
			
		||||
    );
 | 
			
		||||
        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<String>.from(jsonDecode(json['apkUrls'])),
 | 
			
		||||
        preferredApkIndex,
 | 
			
		||||
        additionalSettings,
 | 
			
		||||
        json['lastUpdateCheck'] == null
 | 
			
		||||
            ? null
 | 
			
		||||
            : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
 | 
			
		||||
        json['pinned'] ?? false,
 | 
			
		||||
        categories: json['categories'] != null
 | 
			
		||||
            ? (json['categories'] as List<dynamic>)
 | 
			
		||||
                .map((e) => e.toString())
 | 
			
		||||
                .toList()
 | 
			
		||||
            : json['category'] != null
 | 
			
		||||
                ? [json['category'] as String]
 | 
			
		||||
                : [],
 | 
			
		||||
        releaseDate: json['releaseDate'] == null
 | 
			
		||||
            ? null
 | 
			
		||||
            : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
 | 
			
		||||
        changeLog:
 | 
			
		||||
            json['changeLog'] == null ? null : json['changeLog'] as String);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => {
 | 
			
		||||
@@ -171,7 +180,8 @@ class App {
 | 
			
		||||
        'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
 | 
			
		||||
        'pinned': pinned,
 | 
			
		||||
        'categories': categories,
 | 
			
		||||
        'releaseDate': releaseDate?.microsecondsSinceEpoch
 | 
			
		||||
        'releaseDate': releaseDate?.microsecondsSinceEpoch,
 | 
			
		||||
        'changeLog': changeLog
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -220,6 +230,7 @@ class AppSource {
 | 
			
		||||
  String? host;
 | 
			
		||||
  late String name;
 | 
			
		||||
  bool enforceTrackOnly = false;
 | 
			
		||||
  bool changeLogIfAnyIsMarkDown = true;
 | 
			
		||||
 | 
			
		||||
  AppSource() {
 | 
			
		||||
    name = runtimeType.toString();
 | 
			
		||||
@@ -332,12 +343,16 @@ class SourceProvider {
 | 
			
		||||
    Codeberg(),
 | 
			
		||||
    FDroid(),
 | 
			
		||||
    IzzyOnDroid(),
 | 
			
		||||
    Mullvad(),
 | 
			
		||||
    Signal(),
 | 
			
		||||
    FDroidRepo(),
 | 
			
		||||
    SourceForge(),
 | 
			
		||||
    APKMirror(),
 | 
			
		||||
    FDroidRepo(),
 | 
			
		||||
    Mullvad(),
 | 
			
		||||
    Signal(),
 | 
			
		||||
    VLC(),
 | 
			
		||||
    // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
 | 
			
		||||
    TelegramApp(),
 | 
			
		||||
    SteamMobile(),
 | 
			
		||||
    NeutronCode(),
 | 
			
		||||
    HTML() // This should ALWAYS be the last option as they are tried in order
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
@@ -433,7 +448,8 @@ class SourceProvider {
 | 
			
		||||
        DateTime.now(),
 | 
			
		||||
        currentApp?.pinned ?? false,
 | 
			
		||||
        categories: currentApp?.categories ?? const [],
 | 
			
		||||
        releaseDate: apk.releaseDate);
 | 
			
		||||
        releaseDate: apk.releaseDate,
 | 
			
		||||
        changeLog: apk.changeLog);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Returns errors in [results, errors] instead of throwing them
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -235,6 +235,14 @@ packages:
 | 
			
		||||
    description: flutter
 | 
			
		||||
    source: sdk
 | 
			
		||||
    version: "0.0.0"
 | 
			
		||||
  flutter_markdown:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_markdown
 | 
			
		||||
      sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.6.14"
 | 
			
		||||
  flutter_plugin_android_lifecycle:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -325,6 +333,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.1"
 | 
			
		||||
  markdown:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: markdown
 | 
			
		||||
      sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.0.1"
 | 
			
		||||
  matcher:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -702,10 +718,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_ios
 | 
			
		||||
      sha256: "7ab1e5b646623d6a2537aa59d5d039f90eebef75a7c25e105f6f75de1f7750c3"
 | 
			
		||||
      sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.2"
 | 
			
		||||
    version: "6.1.3"
 | 
			
		||||
  url_launcher_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -766,10 +782,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter
 | 
			
		||||
      sha256: b6cd42db3ced5411f3d01599906156885b18e4188f7065a8a351eb84bee347e0
 | 
			
		||||
      sha256: "47663d51a9061451aa3880a214ee9a65dcbb933b77bc44388e194279ab3ccaf6"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.0.6"
 | 
			
		||||
    version: "4.0.7"
 | 
			
		||||
  webview_flutter_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -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.11.9+130 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.11.15+136 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=2.18.2 <3.0.0'
 | 
			
		||||
@@ -59,6 +59,7 @@ dependencies:
 | 
			
		||||
  sqflite: ^2.2.0+3
 | 
			
		||||
  easy_localization: ^3.0.1
 | 
			
		||||
  android_intent_plus: ^3.1.5
 | 
			
		||||
  flutter_markdown: ^0.6.14
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
@@ -91,6 +92,7 @@ flutter:
 | 
			
		||||
  assets:
 | 
			
		||||
    - assets/translations/
 | 
			
		||||
    - assets/graphics/
 | 
			
		||||
    - assets/ca/
 | 
			
		||||
 | 
			
		||||
  # An image asset can refer to one or more resolution-specific "variants", see
 | 
			
		||||
  # https://flutter.dev/assets-and-images/#resolution-aware
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user