Compare commits
	
		
			17 Commits
		
	
	
		
			v0.5.4-bet
			...
			v0.6.2-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9ef26b3a4a | ||
| 
						 | 
					27ee6b9e88 | ||
| 
						 | 
					d1a3529036 | ||
| 
						 | 
					a954a627fd | ||
| 
						 | 
					52ce5b19c4 | ||
| 
						 | 
					03f0b6cf05 | ||
| 
						 | 
					5d8d0de8de | ||
| 
						 | 
					07f6d4ad2c | ||
| 
						 | 
					dfbb4e19a5 | ||
| 
						 | 
					f5fda2ca90 | ||
| 
						 | 
					661dc1626c | ||
| 
						 | 
					dde3fc20fb | ||
| 
						 | 
					017b867d8d | ||
| 
						 | 
					1cb1c124eb | ||
| 
						 | 
					fdeb852c7b | ||
| 
						 | 
					67f50ba776 | ||
| 
						 | 
					a0968caa5c | 
@@ -30,16 +30,6 @@
 | 
				
			|||||||
        <meta-data
 | 
					        <meta-data
 | 
				
			||||||
            android:name="flutterEmbedding"
 | 
					            android:name="flutterEmbedding"
 | 
				
			||||||
            android:value="2" />
 | 
					            android:value="2" />
 | 
				
			||||||
        <provider
 | 
					 | 
				
			||||||
            android:name="androidx.core.content.FileProvider"
 | 
					 | 
				
			||||||
            android:authorities="${applicationId}.fileProvider"
 | 
					 | 
				
			||||||
            android:exported="false"
 | 
					 | 
				
			||||||
            android:grantUriPermissions="true">
 | 
					 | 
				
			||||||
            <meta-data
 | 
					 | 
				
			||||||
                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
					 | 
				
			||||||
                android:resource="@xml/file_paths" />
 | 
					 | 
				
			||||||
        </provider>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    </application>
 | 
					    </application>
 | 
				
			||||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
					    <uses-permission android:name="android.permission.INTERNET" />
 | 
				
			||||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
					    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 228 KiB  | 
| 
		 Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 162 KiB  | 
| 
		 Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 170 KiB  | 
| 
		 Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 146 KiB  | 
| 
		 Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 188 KiB  | 
| 
		 Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB  | 
							
								
								
									
										112
									
								
								lib/app_sources/apkmirror.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
 | 
					import 'package:http/http.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class APKMirror implements AppSource {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  late String host = 'apkmirror.com';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String standardizeURL(String url) {
 | 
				
			||||||
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
 | 
				
			||||||
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
 | 
					    if (match == null) {
 | 
				
			||||||
 | 
					      throw notValidURL(runtimeType.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
 | 
					      '$standardUrl#whatsnew';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async {
 | 
				
			||||||
 | 
					    var originalUri = Uri.parse(apkUrl);
 | 
				
			||||||
 | 
					    var res = await get(originalUri);
 | 
				
			||||||
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var href =
 | 
				
			||||||
 | 
					        parse(res.body).querySelector('.downloadButton')?.attributes['href'];
 | 
				
			||||||
 | 
					    if (href == null) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var res2 = await get(Uri.parse('${originalUri.origin}$href'), headers: {
 | 
				
			||||||
 | 
					      'User-Agent':
 | 
				
			||||||
 | 
					          'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res2.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var links = parse(res2.body)
 | 
				
			||||||
 | 
					        .querySelectorAll('a')
 | 
				
			||||||
 | 
					        .where((element) => element.innerHtml == 'here')
 | 
				
			||||||
 | 
					        .map((e) => e.attributes['href'])
 | 
				
			||||||
 | 
					        .where((element) => element != null)
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					    if (links.isEmpty) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return '${originalUri.origin}${links[0]}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 | 
					    Response res = await get(Uri.parse('$standardUrl/feed'));
 | 
				
			||||||
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var nextUrl = parse(res.body)
 | 
				
			||||||
 | 
					        .querySelector('item')
 | 
				
			||||||
 | 
					        ?.querySelector('link')
 | 
				
			||||||
 | 
					        ?.nextElementSibling
 | 
				
			||||||
 | 
					        ?.innerHtml;
 | 
				
			||||||
 | 
					    if (nextUrl == null) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Response res2 = await get(Uri.parse(nextUrl), headers: {
 | 
				
			||||||
 | 
					      'User-Agent':
 | 
				
			||||||
 | 
					          'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res2.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var html2 = parse(res2.body);
 | 
				
			||||||
 | 
					    var origin = Uri.parse(standardUrl).origin;
 | 
				
			||||||
 | 
					    List<String> apkUrls = html2
 | 
				
			||||||
 | 
					        .querySelectorAll('.apkm-badge')
 | 
				
			||||||
 | 
					        .map((e) => e.innerHtml != 'APK'
 | 
				
			||||||
 | 
					            ? ''
 | 
				
			||||||
 | 
					            : e.previousElementSibling?.attributes['href'] ?? '')
 | 
				
			||||||
 | 
					        .where((element) => element.isNotEmpty)
 | 
				
			||||||
 | 
					        .map((e) => '$origin$e')
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					    if (apkUrls.isEmpty) {
 | 
				
			||||||
 | 
					      throw noAPKFound;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var version = html2.querySelector('span.active.accent_color')?.innerHtml;
 | 
				
			||||||
 | 
					    if (version == null) {
 | 
				
			||||||
 | 
					      throw couldNotFindLatestVersion;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return APKDetails(version, apkUrls);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  AppNames getAppNames(String standardUrl) {
 | 
				
			||||||
 | 
					    String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
 | 
				
			||||||
 | 
					    List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
 | 
				
			||||||
 | 
					    return AppNames(names[1], names[2]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<List<GeneratedFormItem>> additionalDataFormItems = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<String> additionalDataDefaults = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<GeneratedFormItem> moreSourceSettingsFormItems = [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -23,28 +23,50 @@ class FDroid implements AppSource {
 | 
				
			|||||||
    return url.substring(0, match.end);
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
    Response res = await get(Uri.parse(standardUrl));
 | 
					    Response res = await get(Uri.parse(standardUrl));
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
      var latestReleaseDiv =
 | 
					      var releases = parse(res.body).querySelectorAll('.package-version');
 | 
				
			||||||
          parse(res.body).querySelector('#latest.package-version');
 | 
					      if (releases.isEmpty) {
 | 
				
			||||||
      var apkUrl = latestReleaseDiv
 | 
					        throw couldNotFindReleases;
 | 
				
			||||||
          ?.querySelector('.package-version-download a')
 | 
					 | 
				
			||||||
          ?.attributes['href'];
 | 
					 | 
				
			||||||
      if (apkUrl == null) {
 | 
					 | 
				
			||||||
        throw noAPKFound;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var version = latestReleaseDiv
 | 
					      String? latestVersion = releases[0]
 | 
				
			||||||
          ?.querySelector('.package-version-header b')
 | 
					          .querySelector('.package-version-header b')
 | 
				
			||||||
          ?.innerHtml
 | 
					          ?.innerHtml
 | 
				
			||||||
          .split(' ')
 | 
					          .split(' ')
 | 
				
			||||||
          .last;
 | 
					          .sublist(1)
 | 
				
			||||||
      if (version == null) {
 | 
					          .join(' ');
 | 
				
			||||||
 | 
					      if (latestVersion == null) {
 | 
				
			||||||
        throw couldNotFindLatestVersion;
 | 
					        throw couldNotFindLatestVersion;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return APKDetails(version, [apkUrl]);
 | 
					      List<String> apkUrls = releases
 | 
				
			||||||
 | 
					          .where((element) =>
 | 
				
			||||||
 | 
					              element
 | 
				
			||||||
 | 
					                  .querySelector('.package-version-header b')
 | 
				
			||||||
 | 
					                  ?.innerHtml
 | 
				
			||||||
 | 
					                  .split(' ')
 | 
				
			||||||
 | 
					                  .sublist(1)
 | 
				
			||||||
 | 
					                  .join(' ') ==
 | 
				
			||||||
 | 
					              latestVersion)
 | 
				
			||||||
 | 
					          .map((e) =>
 | 
				
			||||||
 | 
					              e
 | 
				
			||||||
 | 
					                  .querySelector('.package-version-download a')
 | 
				
			||||||
 | 
					                  ?.attributes['href'] ??
 | 
				
			||||||
 | 
					              '')
 | 
				
			||||||
 | 
					          .where((element) => element.isNotEmpty)
 | 
				
			||||||
 | 
					          .toList();
 | 
				
			||||||
 | 
					      if (apkUrls.isEmpty) {
 | 
				
			||||||
 | 
					        throw noAPKFound;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return APKDetails(latestVersion, apkUrls);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      throw couldNotFindReleases;
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,13 @@ class GitHub implements AppSource {
 | 
				
			|||||||
    return creds != null && creds.isNotEmpty ? '$creds@' : '';
 | 
					    return creds != null && creds.isNotEmpty ? '$creds@' : '';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
 | 
					      '$standardUrl/releases';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,13 @@ class GitLab implements AppSource {
 | 
				
			|||||||
    return url.substring(0, match.end);
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
 | 
					      '$standardUrl/-/releases';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,12 @@ class IzzyOnDroid implements AppSource {
 | 
				
			|||||||
    return url.substring(0, match.end);
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,13 @@ class Mullvad implements AppSource {
 | 
				
			|||||||
    return url.substring(0, match.end);
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
 | 
					      'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,12 @@ class Signal implements AppSource {
 | 
				
			|||||||
    return 'https://$host';
 | 
					    return 'https://$host';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,12 @@ class SourceForge implements AppSource {
 | 
				
			|||||||
    return url.substring(0, match.end);
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:obtainium/app_sources/github.dart';
 | 
					 | 
				
			||||||
import 'package:obtainium/custom_errors.dart';
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
import 'package:obtainium/pages/home.dart';
 | 
					import 'package:obtainium/pages/home.dart';
 | 
				
			||||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
@@ -13,12 +14,14 @@ import 'package:workmanager/workmanager.dart';
 | 
				
			|||||||
import 'package:dynamic_color/dynamic_color.dart';
 | 
					import 'package:dynamic_color/dynamic_color.dart';
 | 
				
			||||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
					import 'package:device_info_plus/device_info_plus.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const String currentVersion = '0.6.2';
 | 
				
			||||||
const String currentReleaseTag =
 | 
					const String currentReleaseTag =
 | 
				
			||||||
    'v0.5.4-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
					    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const String bgUpdateCheckTaskName = 'bg-update-check';
 | 
					const String bgUpdateCheckTaskName = 'bg-update-check';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bgUpdateCheck(int? ignoreAfterMicroseconds) async {
 | 
					bgUpdateCheck(int? ignoreAfterMicroseconds) async {
 | 
				
			||||||
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  DateTime? ignoreAfter = ignoreAfterMicroseconds != null
 | 
					  DateTime? ignoreAfter = ignoreAfterMicroseconds != null
 | 
				
			||||||
      ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
 | 
					      ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
 | 
				
			||||||
      : null;
 | 
					      : null;
 | 
				
			||||||
@@ -27,22 +30,28 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    var appsProvider = AppsProvider();
 | 
					    var appsProvider = AppsProvider();
 | 
				
			||||||
    await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
 | 
					    await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
 | 
				
			||||||
    await appsProvider.loadApps();
 | 
					    await appsProvider.loadApps(shouldCorrectInstallStatus: false);
 | 
				
			||||||
    List<String> existingUpdateIds =
 | 
					    List<String> existingUpdateIds =
 | 
				
			||||||
        appsProvider.getExistingUpdates(installedOnly: true);
 | 
					        appsProvider.getExistingUpdates(installedOnly: true);
 | 
				
			||||||
    DateTime nextIgnoreAfter = DateTime.now();
 | 
					    DateTime nextIgnoreAfter = DateTime.now();
 | 
				
			||||||
 | 
					    String? err;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await appsProvider.checkUpdates(ignoreAfter: ignoreAfter);
 | 
					      await appsProvider.checkUpdates(
 | 
				
			||||||
 | 
					          ignoreAfter: ignoreAfter,
 | 
				
			||||||
 | 
					          immediatelyThrowRateLimitError: true,
 | 
				
			||||||
 | 
					          immediatelyThrowSocketError: true,
 | 
				
			||||||
 | 
					          shouldCorrectInstallStatus: false);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      if (e is RateLimitError) {
 | 
					      if (e is RateLimitError || e is SocketException) {
 | 
				
			||||||
        String nextTaskName =
 | 
					        String nextTaskName =
 | 
				
			||||||
            '$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
 | 
					            '$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
 | 
				
			||||||
        Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
 | 
					        Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
 | 
				
			||||||
            constraints: Constraints(networkType: NetworkType.connected),
 | 
					            constraints: Constraints(networkType: NetworkType.connected),
 | 
				
			||||||
            initialDelay: Duration(minutes: e.remainingMinutes),
 | 
					            initialDelay: Duration(
 | 
				
			||||||
 | 
					                minutes: e is RateLimitError ? e.remainingMinutes : 15),
 | 
				
			||||||
            inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
 | 
					            inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        rethrow;
 | 
					        err = e.toString();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    List<App> newUpdates = appsProvider
 | 
					    List<App> newUpdates = appsProvider
 | 
				
			||||||
@@ -66,13 +75,15 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
 | 
				
			|||||||
    // }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newUpdates.isNotEmpty) {
 | 
					    if (newUpdates.isNotEmpty) {
 | 
				
			||||||
      notificationsProvider.notify(UpdateNotification(newUpdates),
 | 
					      notificationsProvider.notify(UpdateNotification(newUpdates));
 | 
				
			||||||
          cancelExisting: true);
 | 
					    }
 | 
				
			||||||
 | 
					    if (err != null) {
 | 
				
			||||||
 | 
					      throw err;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return Future.value(true);
 | 
					    return Future.value(true);
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
    notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()),
 | 
					    notificationsProvider
 | 
				
			||||||
        cancelExisting: true);
 | 
					        .notify(ErrorCheckingUpdatesNotification(e.toString()));
 | 
				
			||||||
    return Future.error(false);
 | 
					    return Future.error(false);
 | 
				
			||||||
  } finally {
 | 
					  } finally {
 | 
				
			||||||
    await notificationsProvider.cancel(checkingUpdatesNotification.id);
 | 
					    await notificationsProvider.cancel(checkingUpdatesNotification.id);
 | 
				
			||||||
@@ -89,7 +100,7 @@ void bgTaskCallback() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void main() async {
 | 
					void main() async {
 | 
				
			||||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) {
 | 
					  if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
 | 
				
			||||||
    SystemChrome.setSystemUIOverlayStyle(
 | 
					    SystemChrome.setSystemUIOverlayStyle(
 | 
				
			||||||
      const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
 | 
					      const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -138,7 +149,7 @@ class _ObtainiumState extends State<Obtainium> {
 | 
				
			|||||||
        Permission.notification.request();
 | 
					        Permission.notification.request();
 | 
				
			||||||
        appsProvider.saveApps([
 | 
					        appsProvider.saveApps([
 | 
				
			||||||
          App(
 | 
					          App(
 | 
				
			||||||
              'imranr98_obtainium_${GitHub().host}',
 | 
					              'dev.imranr.obtainium',
 | 
				
			||||||
              'https://github.com/ImranR98/Obtainium',
 | 
					              'https://github.com/ImranR98/Obtainium',
 | 
				
			||||||
              'ImranR98',
 | 
					              'ImranR98',
 | 
				
			||||||
              'Obtainium',
 | 
					              'Obtainium',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,6 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
  String userInput = '';
 | 
					  String userInput = '';
 | 
				
			||||||
  AppSource? pickedSource;
 | 
					  AppSource? pickedSource;
 | 
				
			||||||
  List<String> additionalData = [];
 | 
					  List<String> additionalData = [];
 | 
				
			||||||
  String customName = '';
 | 
					 | 
				
			||||||
  bool validAdditionalData = true;
 | 
					  bool validAdditionalData = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -80,9 +79,6 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
                                                .doesSourceHaveRequiredAdditionalData(
 | 
					                                                .doesSourceHaveRequiredAdditionalData(
 | 
				
			||||||
                                                    source)
 | 
					                                                    source)
 | 
				
			||||||
                                            : true;
 | 
					                                            : true;
 | 
				
			||||||
                                        if (source == null) {
 | 
					 | 
				
			||||||
                                          customName = '';
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                      }
 | 
					                                      }
 | 
				
			||||||
                                    });
 | 
					                                    });
 | 
				
			||||||
                                  },
 | 
					                                  },
 | 
				
			||||||
@@ -90,59 +86,76 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
                          const SizedBox(
 | 
					                          const SizedBox(
 | 
				
			||||||
                            width: 16,
 | 
					                            width: 16,
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          ElevatedButton(
 | 
					                          gettingAppInfo
 | 
				
			||||||
                              onPressed: gettingAppInfo ||
 | 
					                              ? const CircularProgressIndicator()
 | 
				
			||||||
                                      pickedSource == null ||
 | 
					                              : ElevatedButton(
 | 
				
			||||||
                                      (pickedSource!.additionalDataFormItems
 | 
					                                  onPressed: gettingAppInfo ||
 | 
				
			||||||
                                              .isNotEmpty &&
 | 
					                                          pickedSource == null ||
 | 
				
			||||||
                                          !validAdditionalData)
 | 
					                                          (pickedSource!.additionalDataFormItems
 | 
				
			||||||
                                  ? null
 | 
					                                                  .isNotEmpty &&
 | 
				
			||||||
                                  : () {
 | 
					                                              !validAdditionalData)
 | 
				
			||||||
                                      HapticFeedback.selectionClick();
 | 
					                                      ? null
 | 
				
			||||||
                                      setState(() {
 | 
					                                      : () async {
 | 
				
			||||||
                                        gettingAppInfo = true;
 | 
					                                          setState(() {
 | 
				
			||||||
                                      });
 | 
					                                            gettingAppInfo = true;
 | 
				
			||||||
                                      sourceProvider
 | 
					                                          });
 | 
				
			||||||
                                          .getApp(pickedSource!, userInput,
 | 
					                                          var appsProvider =
 | 
				
			||||||
                                              additionalData,
 | 
					                                              context.read<AppsProvider>();
 | 
				
			||||||
                                              customName: customName)
 | 
					                                          var settingsProvider =
 | 
				
			||||||
                                          .then((app) {
 | 
					                                              context.read<SettingsProvider>();
 | 
				
			||||||
                                        var appsProvider =
 | 
					                                          () async {
 | 
				
			||||||
                                            context.read<AppsProvider>();
 | 
					                                            HapticFeedback.selectionClick();
 | 
				
			||||||
                                        var settingsProvider =
 | 
					                                            App app =
 | 
				
			||||||
                                            context.read<SettingsProvider>();
 | 
					                                                await sourceProvider.getApp(
 | 
				
			||||||
                                        if (appsProvider.apps
 | 
					                                                    pickedSource!,
 | 
				
			||||||
                                            .containsKey(app.id)) {
 | 
					                                                    userInput,
 | 
				
			||||||
                                          throw 'App already added';
 | 
					                                                    additionalData);
 | 
				
			||||||
                                        }
 | 
					                                            await settingsProvider
 | 
				
			||||||
                                        settingsProvider
 | 
					                                                .getInstallPermission();
 | 
				
			||||||
                                            .getInstallPermission()
 | 
					                                            // ignore: use_build_context_synchronously
 | 
				
			||||||
                                            .then((_) {
 | 
					                                            var apkUrl = await appsProvider
 | 
				
			||||||
                                          appsProvider
 | 
					                                                .selectApkUrl(app, context);
 | 
				
			||||||
                                              .saveApps([app]).then((_) {
 | 
					                                            if (apkUrl == null) {
 | 
				
			||||||
 | 
					                                              throw 'Cancelled';
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            app.preferredApkIndex =
 | 
				
			||||||
 | 
					                                                app.apkUrls.indexOf(apkUrl);
 | 
				
			||||||
 | 
					                                            var downloadedApk =
 | 
				
			||||||
 | 
					                                                await appsProvider
 | 
				
			||||||
 | 
					                                                    .downloadApp(app);
 | 
				
			||||||
 | 
					                                            app.id = downloadedApk.appId;
 | 
				
			||||||
 | 
					                                            if (appsProvider.apps
 | 
				
			||||||
 | 
					                                                .containsKey(app.id)) {
 | 
				
			||||||
 | 
					                                              throw 'App already added';
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            await appsProvider.saveApps([app]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            return app;
 | 
				
			||||||
 | 
					                                          }()
 | 
				
			||||||
 | 
					                                              .then((app) {
 | 
				
			||||||
                                            Navigator.push(
 | 
					                                            Navigator.push(
 | 
				
			||||||
                                                context,
 | 
					                                                context,
 | 
				
			||||||
                                                MaterialPageRoute(
 | 
					                                                MaterialPageRoute(
 | 
				
			||||||
                                                    builder: (context) =>
 | 
					                                                    builder: (context) =>
 | 
				
			||||||
                                                        AppPage(
 | 
					                                                        AppPage(
 | 
				
			||||||
                                                            appId: app.id)));
 | 
					                                                            appId: app.id)));
 | 
				
			||||||
 | 
					                                          }).catchError((e) {
 | 
				
			||||||
 | 
					                                            ScaffoldMessenger.of(context)
 | 
				
			||||||
 | 
					                                                .showSnackBar(
 | 
				
			||||||
 | 
					                                              SnackBar(
 | 
				
			||||||
 | 
					                                                  content: Text(e.toString())),
 | 
				
			||||||
 | 
					                                            );
 | 
				
			||||||
 | 
					                                          }).whenComplete(() {
 | 
				
			||||||
 | 
					                                            setState(() {
 | 
				
			||||||
 | 
					                                              gettingAppInfo = false;
 | 
				
			||||||
 | 
					                                            });
 | 
				
			||||||
                                          });
 | 
					                                          });
 | 
				
			||||||
                                        });
 | 
					                                        },
 | 
				
			||||||
                                      }).catchError((e) {
 | 
					                                  child: const Text('Add'))
 | 
				
			||||||
                                        ScaffoldMessenger.of(context)
 | 
					 | 
				
			||||||
                                            .showSnackBar(
 | 
					 | 
				
			||||||
                                          SnackBar(content: Text(e.toString())),
 | 
					 | 
				
			||||||
                                        );
 | 
					 | 
				
			||||||
                                      }).whenComplete(() {
 | 
					 | 
				
			||||||
                                        setState(() {
 | 
					 | 
				
			||||||
                                          gettingAppInfo = false;
 | 
					 | 
				
			||||||
                                        });
 | 
					 | 
				
			||||||
                                      });
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                              child: const Text('Add'))
 | 
					 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      if (pickedSource != null)
 | 
					                      if (pickedSource != null &&
 | 
				
			||||||
 | 
					                          pickedSource!.additionalDataDefaults.isNotEmpty)
 | 
				
			||||||
                        Column(
 | 
					                        Column(
 | 
				
			||||||
                          crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					                          crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
                          children: [
 | 
					                          children: [
 | 
				
			||||||
@@ -174,21 +187,6 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
                              const SizedBox(
 | 
					                              const SizedBox(
 | 
				
			||||||
                                height: 8,
 | 
					                                height: 8,
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            if (pickedSource != null)
 | 
					 | 
				
			||||||
                              GeneratedForm(
 | 
					 | 
				
			||||||
                                  items: [
 | 
					 | 
				
			||||||
                                    [
 | 
					 | 
				
			||||||
                                      GeneratedFormItem(
 | 
					 | 
				
			||||||
                                          label: 'Custom App Name',
 | 
					 | 
				
			||||||
                                          required: false)
 | 
					 | 
				
			||||||
                                    ]
 | 
					 | 
				
			||||||
                                  ],
 | 
					 | 
				
			||||||
                                  onValueChanges: (values, valid) {
 | 
					 | 
				
			||||||
                                    setState(() {
 | 
					 | 
				
			||||||
                                      customName = values[0];
 | 
					 | 
				
			||||||
                                    });
 | 
					 | 
				
			||||||
                                  },
 | 
					 | 
				
			||||||
                                  defaultValues: [customName])
 | 
					 | 
				
			||||||
                          ],
 | 
					                          ],
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                      else
 | 
					                      else
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form.dart';
 | 
					 | 
				
			||||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
					import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			||||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
					import 'package:obtainium/providers/settings_provider.dart';
 | 
				
			||||||
@@ -19,26 +18,34 @@ class AppPage extends StatefulWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _AppPageState extends State<AppPage> {
 | 
					class _AppPageState extends State<AppPage> {
 | 
				
			||||||
 | 
					  AppInMemory? prevApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    var appsProvider = context.watch<AppsProvider>();
 | 
					    var appsProvider = context.watch<AppsProvider>();
 | 
				
			||||||
    var settingsProvider = context.watch<SettingsProvider>();
 | 
					    var settingsProvider = context.watch<SettingsProvider>();
 | 
				
			||||||
    var sourceProvider = SourceProvider();
 | 
					    getUpdate(String id) {
 | 
				
			||||||
    AppInMemory? app = appsProvider.apps[widget.appId];
 | 
					      appsProvider.getUpdate(id).catchError((e) {
 | 
				
			||||||
    var source = app != null ? sourceProvider.getSource(app.app.url) : null;
 | 
					 | 
				
			||||||
    if (!appsProvider.areDownloadsRunning() && app != null) {
 | 
					 | 
				
			||||||
      appsProvider.getUpdate(app.app.id).catchError((e) {
 | 
					 | 
				
			||||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
					        ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
          SnackBar(content: Text(e.toString())),
 | 
					          SnackBar(content: Text(e.toString())),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var sourceProvider = SourceProvider();
 | 
				
			||||||
 | 
					    AppInMemory? app = appsProvider.apps[widget.appId];
 | 
				
			||||||
 | 
					    var source = app != null ? sourceProvider.getSource(app.app.url) : null;
 | 
				
			||||||
 | 
					    if (!appsProvider.areDownloadsRunning() && prevApp == null && app != null) {
 | 
				
			||||||
 | 
					      prevApp = app;
 | 
				
			||||||
 | 
					      getUpdate(app.app.id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: settingsProvider.showAppWebpage ? AppBar() : null,
 | 
					      appBar: settingsProvider.showAppWebpage ? AppBar() : null,
 | 
				
			||||||
      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
					      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
      body: RefreshIndicator(
 | 
					      body: RefreshIndicator(
 | 
				
			||||||
          child: settingsProvider.showAppWebpage
 | 
					          child: settingsProvider.showAppWebpage
 | 
				
			||||||
              ? WebView(
 | 
					              ? WebView(
 | 
				
			||||||
 | 
					                  backgroundColor: Theme.of(context).colorScheme.background,
 | 
				
			||||||
                  initialUrl: app?.app.url,
 | 
					                  initialUrl: app?.app.url,
 | 
				
			||||||
                  javascriptMode: JavascriptMode.unrestricted,
 | 
					                  javascriptMode: JavascriptMode.unrestricted,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -49,8 +56,18 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                      mainAxisAlignment: MainAxisAlignment.center,
 | 
					                      mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                      crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					                      crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        app?.installedInfo != null
 | 
				
			||||||
 | 
					                            ? Row(
 | 
				
			||||||
 | 
					                                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                                children: [
 | 
				
			||||||
 | 
					                                    Image.memory(
 | 
				
			||||||
 | 
					                                      app!.installedInfo!.icon!,
 | 
				
			||||||
 | 
					                                      scale: 1.5,
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                  ])
 | 
				
			||||||
 | 
					                            : Container(),
 | 
				
			||||||
                        Text(
 | 
					                        Text(
 | 
				
			||||||
                          app?.app.name ?? 'App',
 | 
					                          app?.installedInfo?.name ?? app?.app.name ?? 'App',
 | 
				
			||||||
                          textAlign: TextAlign.center,
 | 
					                          textAlign: TextAlign.center,
 | 
				
			||||||
                          style: Theme.of(context).textTheme.displayLarge,
 | 
					                          style: Theme.of(context).textTheme.displayLarge,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
@@ -105,13 +122,7 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
          onRefresh: () async {
 | 
					          onRefresh: () async {
 | 
				
			||||||
            if (app != null) {
 | 
					            if (app != null) {
 | 
				
			||||||
              try {
 | 
					              getUpdate(app.app.id);
 | 
				
			||||||
                await appsProvider.getUpdate(app.app.id);
 | 
					 | 
				
			||||||
              } catch (e) {
 | 
					 | 
				
			||||||
                ScaffoldMessenger.of(context).showSnackBar(
 | 
					 | 
				
			||||||
                  SnackBar(content: Text(e.toString())),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
      bottomSheet: Padding(
 | 
					      bottomSheet: Padding(
 | 
				
			||||||
@@ -125,7 +136,8 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                  child: Row(
 | 
					                  child: Row(
 | 
				
			||||||
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
					                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        if (app?.app.installedVersion != app?.app.latestVersion)
 | 
					                        if (app?.app.installedVersion != null &&
 | 
				
			||||||
 | 
					                            app?.app.installedVersion != app?.app.latestVersion)
 | 
				
			||||||
                          IconButton(
 | 
					                          IconButton(
 | 
				
			||||||
                              onPressed: app?.downloadProgress != null
 | 
					                              onPressed: app?.downloadProgress != null
 | 
				
			||||||
                                  ? null
 | 
					                                  ? null
 | 
				
			||||||
@@ -134,8 +146,8 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                                          context: context,
 | 
					                                          context: context,
 | 
				
			||||||
                                          builder: (BuildContext ctx) {
 | 
					                                          builder: (BuildContext ctx) {
 | 
				
			||||||
                                            return AlertDialog(
 | 
					                                            return AlertDialog(
 | 
				
			||||||
                                              title: Text(
 | 
					                                              title: const Text(
 | 
				
			||||||
                                                  'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
 | 
					                                                  'App Already up to Date?'),
 | 
				
			||||||
                                              actions: [
 | 
					                                              actions: [
 | 
				
			||||||
                                                TextButton(
 | 
					                                                TextButton(
 | 
				
			||||||
                                                    onPressed: () {
 | 
					                                                    onPressed: () {
 | 
				
			||||||
@@ -160,54 +172,13 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                                                          .pop();
 | 
					                                                          .pop();
 | 
				
			||||||
                                                    },
 | 
					                                                    },
 | 
				
			||||||
                                                    child: const Text(
 | 
					                                                    child: const Text(
 | 
				
			||||||
                                                        'Yes, Mark as Installed'))
 | 
					                                                        'Yes, Mark as Updated'))
 | 
				
			||||||
                                              ],
 | 
					                                              ],
 | 
				
			||||||
                                            );
 | 
					                                            );
 | 
				
			||||||
                                          });
 | 
					                                          });
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                              tooltip: 'Mark as Installed',
 | 
					                              tooltip: 'Mark as Updated',
 | 
				
			||||||
                              icon: const Icon(Icons.done))
 | 
					                              icon: const Icon(Icons.done)),
 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                          IconButton(
 | 
					 | 
				
			||||||
                              onPressed: app?.downloadProgress != null
 | 
					 | 
				
			||||||
                                  ? null
 | 
					 | 
				
			||||||
                                  : () {
 | 
					 | 
				
			||||||
                                      showDialog(
 | 
					 | 
				
			||||||
                                          context: context,
 | 
					 | 
				
			||||||
                                          builder: (BuildContext ctx) {
 | 
					 | 
				
			||||||
                                            return AlertDialog(
 | 
					 | 
				
			||||||
                                              title: const Text(
 | 
					 | 
				
			||||||
                                                  'App Not Installed?'),
 | 
					 | 
				
			||||||
                                              actions: [
 | 
					 | 
				
			||||||
                                                TextButton(
 | 
					 | 
				
			||||||
                                                    onPressed: () {
 | 
					 | 
				
			||||||
                                                      Navigator.of(context)
 | 
					 | 
				
			||||||
                                                          .pop();
 | 
					 | 
				
			||||||
                                                    },
 | 
					 | 
				
			||||||
                                                    child: const Text('No')),
 | 
					 | 
				
			||||||
                                                TextButton(
 | 
					 | 
				
			||||||
                                                    onPressed: () {
 | 
					 | 
				
			||||||
                                                      HapticFeedback
 | 
					 | 
				
			||||||
                                                          .selectionClick();
 | 
					 | 
				
			||||||
                                                      var updatedApp = app?.app;
 | 
					 | 
				
			||||||
                                                      if (updatedApp != null) {
 | 
					 | 
				
			||||||
                                                        updatedApp
 | 
					 | 
				
			||||||
                                                                .installedVersion =
 | 
					 | 
				
			||||||
                                                            null;
 | 
					 | 
				
			||||||
                                                        appsProvider.saveApps(
 | 
					 | 
				
			||||||
                                                            [updatedApp]);
 | 
					 | 
				
			||||||
                                                      }
 | 
					 | 
				
			||||||
                                                      Navigator.of(context)
 | 
					 | 
				
			||||||
                                                          .pop();
 | 
					 | 
				
			||||||
                                                    },
 | 
					 | 
				
			||||||
                                                    child: const Text(
 | 
					 | 
				
			||||||
                                                        'Yes, Mark as Not Installed'))
 | 
					 | 
				
			||||||
                                              ],
 | 
					 | 
				
			||||||
                                            );
 | 
					 | 
				
			||||||
                                          });
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                              tooltip: 'Mark as Not Installed',
 | 
					 | 
				
			||||||
                              icon: const Icon(Icons.no_cell_outlined)),
 | 
					 | 
				
			||||||
                        if (source != null &&
 | 
					                        if (source != null &&
 | 
				
			||||||
                            source.additionalDataFormItems.isNotEmpty)
 | 
					                            source.additionalDataFormItems.isNotEmpty)
 | 
				
			||||||
                          IconButton(
 | 
					                          IconButton(
 | 
				
			||||||
@@ -219,32 +190,20 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                                          builder: (BuildContext ctx) {
 | 
					                                          builder: (BuildContext ctx) {
 | 
				
			||||||
                                            return GeneratedFormModal(
 | 
					                                            return GeneratedFormModal(
 | 
				
			||||||
                                                title: 'Additional Options',
 | 
					                                                title: 'Additional Options',
 | 
				
			||||||
                                                items: [
 | 
					                                                items: source
 | 
				
			||||||
                                                  ...source
 | 
					                                                    .additionalDataFormItems,
 | 
				
			||||||
                                                      .additionalDataFormItems,
 | 
					 | 
				
			||||||
                                                  [
 | 
					 | 
				
			||||||
                                                    GeneratedFormItem(
 | 
					 | 
				
			||||||
                                                        label: 'App Name',
 | 
					 | 
				
			||||||
                                                        required: true)
 | 
					 | 
				
			||||||
                                                  ]
 | 
					 | 
				
			||||||
                                                ],
 | 
					 | 
				
			||||||
                                                defaultValues: app != null
 | 
					                                                defaultValues: app != null
 | 
				
			||||||
                                                    ? [
 | 
					                                                    ? app.app.additionalData
 | 
				
			||||||
                                                        ...app
 | 
					                                                    : source
 | 
				
			||||||
                                                            .app.additionalData,
 | 
					                                                        .additionalDataDefaults);
 | 
				
			||||||
                                                        app.app.name
 | 
					 | 
				
			||||||
                                                      ]
 | 
					 | 
				
			||||||
                                                    : [
 | 
					 | 
				
			||||||
                                                        ...source
 | 
					 | 
				
			||||||
                                                            .additionalDataDefaults
 | 
					 | 
				
			||||||
                                                      ]);
 | 
					 | 
				
			||||||
                                          }).then((values) {
 | 
					                                          }).then((values) {
 | 
				
			||||||
                                        if (app != null && values != null) {
 | 
					                                        if (app != null && values != null) {
 | 
				
			||||||
                                          var changedApp = app.app;
 | 
					                                          var changedApp = app.app;
 | 
				
			||||||
                                          var name = values.removeLast();
 | 
					 | 
				
			||||||
                                          changedApp.name = name;
 | 
					 | 
				
			||||||
                                          changedApp.additionalData = values;
 | 
					                                          changedApp.additionalData = values;
 | 
				
			||||||
                                          appsProvider.saveApps([changedApp]);
 | 
					                                          appsProvider.saveApps(
 | 
				
			||||||
 | 
					                                              [changedApp]).then((value) {
 | 
				
			||||||
 | 
					                                            getUpdate(changedApp.id);
 | 
				
			||||||
 | 
					                                          });
 | 
				
			||||||
                                        }
 | 
					                                        }
 | 
				
			||||||
                                      });
 | 
					                                      });
 | 
				
			||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
@@ -261,12 +220,18 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                                    ? () {
 | 
					                                    ? () {
 | 
				
			||||||
                                        HapticFeedback.heavyImpact();
 | 
					                                        HapticFeedback.heavyImpact();
 | 
				
			||||||
                                        appsProvider
 | 
					                                        appsProvider
 | 
				
			||||||
                                            .downloadAndInstallLatestApp(
 | 
					                                            .downloadAndInstallLatestApps(
 | 
				
			||||||
                                                [app!.app.id],
 | 
					                                                [app!.app.id],
 | 
				
			||||||
                                                context).then((res) {
 | 
					                                                context).then((res) {
 | 
				
			||||||
                                          if (res.isNotEmpty && mounted) {
 | 
					                                          if (res.isNotEmpty && mounted) {
 | 
				
			||||||
                                            Navigator.of(context).pop();
 | 
					                                            Navigator.of(context).pop();
 | 
				
			||||||
                                          }
 | 
					                                          }
 | 
				
			||||||
 | 
					                                        }).catchError((e) {
 | 
				
			||||||
 | 
					                                          ScaffoldMessenger.of(context)
 | 
				
			||||||
 | 
					                                              .showSnackBar(
 | 
				
			||||||
 | 
					                                            SnackBar(
 | 
				
			||||||
 | 
					                                                content: Text(e.toString())),
 | 
				
			||||||
 | 
					                                          );
 | 
				
			||||||
                                        });
 | 
					                                        });
 | 
				
			||||||
                                      }
 | 
					                                      }
 | 
				
			||||||
                                    : null,
 | 
					                                    : null,
 | 
				
			||||||
@@ -284,7 +249,7 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                                        return AlertDialog(
 | 
					                                        return AlertDialog(
 | 
				
			||||||
                                          title: const Text('Remove App?'),
 | 
					                                          title: const Text('Remove App?'),
 | 
				
			||||||
                                          content: Text(
 | 
					                                          content: Text(
 | 
				
			||||||
                                              'This will remove \'${app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
 | 
					                                              'This will remove \'${app?.installedInfo?.name ?? app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
 | 
				
			||||||
                                          actions: [
 | 
					                                          actions: [
 | 
				
			||||||
                                            TextButton(
 | 
					                                            TextButton(
 | 
				
			||||||
                                                onPressed: () {
 | 
					                                                onPressed: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,10 @@ import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			|||||||
import 'package:obtainium/pages/app.dart';
 | 
					import 'package:obtainium/pages/app.dart';
 | 
				
			||||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
					import 'package:obtainium/providers/settings_provider.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:share_plus/share_plus.dart';
 | 
					import 'package:share_plus/share_plus.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppsPage extends StatefulWidget {
 | 
					class AppsPage extends StatefulWidget {
 | 
				
			||||||
  const AppsPage({super.key});
 | 
					  const AppsPage({super.key});
 | 
				
			||||||
@@ -87,7 +89,8 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
            .toList();
 | 
					            .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (var t in nameTokens) {
 | 
					        for (var t in nameTokens) {
 | 
				
			||||||
          if (!app.app.name.toLowerCase().contains(t.toLowerCase())) {
 | 
					          var name = app.installedInfo?.name ?? app.app.name;
 | 
				
			||||||
 | 
					          if (!name.toLowerCase().contains(t.toLowerCase())) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -101,18 +104,18 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sortedApps.sort((a, b) {
 | 
					    sortedApps.sort((a, b) {
 | 
				
			||||||
 | 
					      var nameA = a.installedInfo?.name ?? a.app.name;
 | 
				
			||||||
 | 
					      var nameB = b.installedInfo?.name ?? b.app.name;
 | 
				
			||||||
      int result = 0;
 | 
					      int result = 0;
 | 
				
			||||||
      if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
 | 
					      if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
 | 
				
			||||||
        result =
 | 
					        result = (a.app.author + nameA).compareTo(b.app.author + nameB);
 | 
				
			||||||
            (a.app.author + a.app.name).compareTo(b.app.author + b.app.name);
 | 
					 | 
				
			||||||
      } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
 | 
					      } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
 | 
				
			||||||
        result =
 | 
					        result = (nameA + a.app.author).compareTo(nameB + b.app.author);
 | 
				
			||||||
            (a.app.name + a.app.author).compareTo(b.app.name + b.app.author);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return result;
 | 
					      return result;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (settingsProvider.sortOrder == SortOrderSettings.ascending) {
 | 
					    if (settingsProvider.sortOrder == SortOrderSettings.descending) {
 | 
				
			||||||
      sortedApps = sortedApps.reversed.toList();
 | 
					      sortedApps = sortedApps.reversed.toList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -164,7 +167,11 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                onLongPress: () {
 | 
					                onLongPress: () {
 | 
				
			||||||
                  toggleAppSelected(sortedApps[index].app.id);
 | 
					                  toggleAppSelected(sortedApps[index].app.id);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                title: Text(sortedApps[index].app.name),
 | 
					                leading: sortedApps[index].installedInfo != null
 | 
				
			||||||
 | 
					                    ? Image.memory(sortedApps[index].installedInfo!.icon!)
 | 
				
			||||||
 | 
					                    : null,
 | 
				
			||||||
 | 
					                title: Text(sortedApps[index].installedInfo?.name ??
 | 
				
			||||||
 | 
					                    sortedApps[index].app.name),
 | 
				
			||||||
                subtitle: Text('By ${sortedApps[index].app.author}'),
 | 
					                subtitle: Text('By ${sortedApps[index].app.author}'),
 | 
				
			||||||
                trailing: sortedApps[index].downloadProgress != null
 | 
					                trailing: sortedApps[index].downloadProgress != null
 | 
				
			||||||
                    ? Text(
 | 
					                    ? Text(
 | 
				
			||||||
@@ -172,7 +179,39 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                    : (sortedApps[index].app.installedVersion != null &&
 | 
					                    : (sortedApps[index].app.installedVersion != null &&
 | 
				
			||||||
                            sortedApps[index].app.installedVersion !=
 | 
					                            sortedApps[index].app.installedVersion !=
 | 
				
			||||||
                                sortedApps[index].app.latestVersion
 | 
					                                sortedApps[index].app.latestVersion
 | 
				
			||||||
                        ? const Text('Update Available')
 | 
					                        ? Column(
 | 
				
			||||||
 | 
					                            mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                            crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Text(appsProvider.areDownloadsRunning()
 | 
				
			||||||
 | 
					                                  ? 'Please Wait...'
 | 
				
			||||||
 | 
					                                  : 'Update Available'),
 | 
				
			||||||
 | 
					                              SourceProvider()
 | 
				
			||||||
 | 
					                                          .getSource(sortedApps[index].app.url)
 | 
				
			||||||
 | 
					                                          .changeLogPageFromStandardUrl(
 | 
				
			||||||
 | 
					                                              sortedApps[index].app.url) ==
 | 
				
			||||||
 | 
					                                      null
 | 
				
			||||||
 | 
					                                  ? const SizedBox()
 | 
				
			||||||
 | 
					                                  : GestureDetector(
 | 
				
			||||||
 | 
					                                      onTap: () {
 | 
				
			||||||
 | 
					                                        launchUrlString(
 | 
				
			||||||
 | 
					                                            SourceProvider()
 | 
				
			||||||
 | 
					                                                .getSource(
 | 
				
			||||||
 | 
					                                                    sortedApps[index].app.url)
 | 
				
			||||||
 | 
					                                                .changeLogPageFromStandardUrl(
 | 
				
			||||||
 | 
					                                                    sortedApps[index].app.url)!,
 | 
				
			||||||
 | 
					                                            mode:
 | 
				
			||||||
 | 
					                                                LaunchMode.externalApplication);
 | 
				
			||||||
 | 
					                                      },
 | 
				
			||||||
 | 
					                                      child: const Text(
 | 
				
			||||||
 | 
					                                        'See Changes',
 | 
				
			||||||
 | 
					                                        style: TextStyle(
 | 
				
			||||||
 | 
					                                            fontStyle: FontStyle.italic,
 | 
				
			||||||
 | 
					                                            decoration:
 | 
				
			||||||
 | 
					                                                TextDecoration.underline),
 | 
				
			||||||
 | 
					                                      )),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
                        : Text(sortedApps[index].app.installedVersion ??
 | 
					                        : Text(sortedApps[index].app.installedVersion ??
 | 
				
			||||||
                            'Not Installed')),
 | 
					                            'Not Installed')),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
@@ -271,15 +310,20 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                                    message:
 | 
					                                    message:
 | 
				
			||||||
                                        '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
 | 
					                                        '${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
 | 
				
			||||||
                                    items: formInputs,
 | 
					                                    items: formInputs,
 | 
				
			||||||
                                    defaultValues: const ['true', 'true'],
 | 
					                                    defaultValues: [
 | 
				
			||||||
 | 
					                                      'true',
 | 
				
			||||||
 | 
					                                      existingUpdateIdsAllOrSelected.isEmpty
 | 
				
			||||||
 | 
					                                          ? 'true'
 | 
				
			||||||
 | 
					                                          : ''
 | 
				
			||||||
 | 
					                                    ],
 | 
				
			||||||
                                    initValid: true,
 | 
					                                    initValid: true,
 | 
				
			||||||
                                  );
 | 
					                                  );
 | 
				
			||||||
                                }).then((values) {
 | 
					                                }).then((values) {
 | 
				
			||||||
                              if (values != null) {
 | 
					                              if (values != null) {
 | 
				
			||||||
                                bool shouldInstallUpdates =
 | 
					                                bool shouldInstallUpdates =
 | 
				
			||||||
                                    values.length < 2 || values[0] == 'true';
 | 
					                                    values.isEmpty || values[0] == 'true';
 | 
				
			||||||
                                bool shouldInstallNew =
 | 
					                                bool shouldInstallNew = values.isEmpty ||
 | 
				
			||||||
                                    values.length < 2 || values[1] == 'true';
 | 
					                                    (values.length >= 2 && values[1] == 'true');
 | 
				
			||||||
                                settingsProvider
 | 
					                                settingsProvider
 | 
				
			||||||
                                    .getInstallPermission()
 | 
					                                    .getInstallPermission()
 | 
				
			||||||
                                    .then((_) {
 | 
					                                    .then((_) {
 | 
				
			||||||
@@ -292,8 +336,14 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                                    toInstall
 | 
					                                    toInstall
 | 
				
			||||||
                                        .addAll(newInstallIdsAllOrSelected);
 | 
					                                        .addAll(newInstallIdsAllOrSelected);
 | 
				
			||||||
                                  }
 | 
					                                  }
 | 
				
			||||||
                                  appsProvider.downloadAndInstallLatestApp(
 | 
					                                  appsProvider
 | 
				
			||||||
                                      toInstall, context);
 | 
					                                      .downloadAndInstallLatestApps(
 | 
				
			||||||
 | 
					                                          toInstall, context)
 | 
				
			||||||
 | 
					                                      .catchError((e) {
 | 
				
			||||||
 | 
					                                    ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
 | 
					                                      SnackBar(content: Text(e.toString())),
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                  });
 | 
				
			||||||
                                });
 | 
					                                });
 | 
				
			||||||
                              }
 | 
					                              }
 | 
				
			||||||
                            });
 | 
					                            });
 | 
				
			||||||
@@ -308,16 +358,94 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                    : IconButton(
 | 
					                    : IconButton(
 | 
				
			||||||
                        visualDensity: VisualDensity.compact,
 | 
					                        visualDensity: VisualDensity.compact,
 | 
				
			||||||
                        onPressed: () {
 | 
					                        onPressed: () {
 | 
				
			||||||
                          String urls = '';
 | 
					                          showDialog(
 | 
				
			||||||
                          for (var id in selectedIds) {
 | 
					                              context: context,
 | 
				
			||||||
                            urls += '${appsProvider.apps[id]!.app.url}\n';
 | 
					                              builder: (BuildContext ctx) {
 | 
				
			||||||
                          }
 | 
					                                return AlertDialog(
 | 
				
			||||||
                          urls = urls.substring(0, urls.length - 1);
 | 
					                                  scrollable: true,
 | 
				
			||||||
                          Share.share(urls,
 | 
					                                  content: Padding(
 | 
				
			||||||
                              subject: 'Selected App URLs from Obtainium');
 | 
					                                    padding: const EdgeInsets.only(top: 6),
 | 
				
			||||||
 | 
					                                    child: Row(
 | 
				
			||||||
 | 
					                                        mainAxisAlignment:
 | 
				
			||||||
 | 
					                                            MainAxisAlignment.spaceAround,
 | 
				
			||||||
 | 
					                                        children: [
 | 
				
			||||||
 | 
					                                          IconButton(
 | 
				
			||||||
 | 
					                                              onPressed:
 | 
				
			||||||
 | 
					                                                  appsProvider
 | 
				
			||||||
 | 
					                                                          .areDownloadsRunning()
 | 
				
			||||||
 | 
					                                                      ? null
 | 
				
			||||||
 | 
					                                                      : () {
 | 
				
			||||||
 | 
					                                                          showDialog(
 | 
				
			||||||
 | 
					                                                              context: context,
 | 
				
			||||||
 | 
					                                                              builder:
 | 
				
			||||||
 | 
					                                                                  (BuildContext
 | 
				
			||||||
 | 
					                                                                      ctx) {
 | 
				
			||||||
 | 
					                                                                return AlertDialog(
 | 
				
			||||||
 | 
					                                                                  title: Text(
 | 
				
			||||||
 | 
					                                                                      'Mark ${selectedIds.length} Selected Apps as Updated?'),
 | 
				
			||||||
 | 
					                                                                  content:
 | 
				
			||||||
 | 
					                                                                      const Text(
 | 
				
			||||||
 | 
					                                                                          'Only applies to installed but out of date Apps.'),
 | 
				
			||||||
 | 
					                                                                  actions: [
 | 
				
			||||||
 | 
					                                                                    TextButton(
 | 
				
			||||||
 | 
					                                                                        onPressed:
 | 
				
			||||||
 | 
					                                                                            () {
 | 
				
			||||||
 | 
					                                                                          Navigator.of(context)
 | 
				
			||||||
 | 
					                                                                              .pop();
 | 
				
			||||||
 | 
					                                                                        },
 | 
				
			||||||
 | 
					                                                                        child: const Text(
 | 
				
			||||||
 | 
					                                                                            'No')),
 | 
				
			||||||
 | 
					                                                                    TextButton(
 | 
				
			||||||
 | 
					                                                                        onPressed:
 | 
				
			||||||
 | 
					                                                                            () {
 | 
				
			||||||
 | 
					                                                                          HapticFeedback
 | 
				
			||||||
 | 
					                                                                              .selectionClick();
 | 
				
			||||||
 | 
					                                                                          appsProvider
 | 
				
			||||||
 | 
					                                                                              .saveApps(selectedIds.map((e) {
 | 
				
			||||||
 | 
					                                                                            var a =
 | 
				
			||||||
 | 
					                                                                                appsProvider.apps[e]!.app;
 | 
				
			||||||
 | 
					                                                                            if (a.installedVersion !=
 | 
				
			||||||
 | 
					                                                                                null) {
 | 
				
			||||||
 | 
					                                                                              a.installedVersion = a.latestVersion;
 | 
				
			||||||
 | 
					                                                                            }
 | 
				
			||||||
 | 
					                                                                            return a;
 | 
				
			||||||
 | 
					                                                                          }).toList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                                                          Navigator.of(context)
 | 
				
			||||||
 | 
					                                                                              .pop();
 | 
				
			||||||
 | 
					                                                                        },
 | 
				
			||||||
 | 
					                                                                        child: const Text(
 | 
				
			||||||
 | 
					                                                                            'Yes'))
 | 
				
			||||||
 | 
					                                                                  ],
 | 
				
			||||||
 | 
					                                                                );
 | 
				
			||||||
 | 
					                                                              });
 | 
				
			||||||
 | 
					                                                        },
 | 
				
			||||||
 | 
					                                              tooltip:
 | 
				
			||||||
 | 
					                                                  'Mark Selected Apps as Updated',
 | 
				
			||||||
 | 
					                                              icon: const Icon(Icons.done)),
 | 
				
			||||||
 | 
					                                          IconButton(
 | 
				
			||||||
 | 
					                                            onPressed: () {
 | 
				
			||||||
 | 
					                                              String urls = '';
 | 
				
			||||||
 | 
					                                              for (var id in selectedIds) {
 | 
				
			||||||
 | 
					                                                urls +=
 | 
				
			||||||
 | 
					                                                    '${appsProvider.apps[id]!.app.url}\n';
 | 
				
			||||||
 | 
					                                              }
 | 
				
			||||||
 | 
					                                              urls = urls.substring(
 | 
				
			||||||
 | 
					                                                  0, urls.length - 1);
 | 
				
			||||||
 | 
					                                              Share.share(urls,
 | 
				
			||||||
 | 
					                                                  subject:
 | 
				
			||||||
 | 
					                                                      '${selectedIds.length} Selected App URLs from Obtainium');
 | 
				
			||||||
 | 
					                                            },
 | 
				
			||||||
 | 
					                                            tooltip: 'Share Selected App URLs',
 | 
				
			||||||
 | 
					                                            icon: const Icon(Icons.share),
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
 | 
					                                        ]),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                              });
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        tooltip: 'Share Selected App URLs',
 | 
					                        tooltip: 'More',
 | 
				
			||||||
                        icon: const Icon(Icons.share),
 | 
					                        icon: const Icon(Icons.more_horiz),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            )),
 | 
					            )),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,26 +8,31 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
					import 'package:device_info_plus/device_info_plus.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:install_plugin_v2/install_plugin_v2.dart';
 | 
				
			||||||
 | 
					import 'package:installed_apps/app_info.dart';
 | 
				
			||||||
 | 
					import 'package:installed_apps/installed_apps.dart';
 | 
				
			||||||
import 'package:obtainium/app_sources/github.dart';
 | 
					import 'package:obtainium/app_sources/github.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
import 'package:obtainium/providers/notifications_provider.dart';
 | 
					import 'package:obtainium/providers/notifications_provider.dart';
 | 
				
			||||||
 | 
					import 'package:package_archive_info/package_archive_info.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:path_provider/path_provider.dart';
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
 | 
					import 'package:flutter_fgbg/flutter_fgbg.dart';
 | 
				
			||||||
import 'package:obtainium/providers/source_provider.dart';
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
import 'package:http/http.dart';
 | 
					import 'package:http/http.dart';
 | 
				
			||||||
import 'package:flutter_install_app/flutter_install_app.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppInMemory {
 | 
					class AppInMemory {
 | 
				
			||||||
  late App app;
 | 
					  late App app;
 | 
				
			||||||
  double? downloadProgress;
 | 
					  double? downloadProgress;
 | 
				
			||||||
 | 
					  AppInfo? installedInfo; // Also indicates that an App is installed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AppInMemory(this.app, this.downloadProgress);
 | 
					  AppInMemory(this.app, this.downloadProgress, this.installedInfo);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApkFile {
 | 
					class DownloadedApp {
 | 
				
			||||||
  String appId;
 | 
					  String appId;
 | 
				
			||||||
  File file;
 | 
					  File file;
 | 
				
			||||||
  ApkFile(this.appId, this.file);
 | 
					  DownloadedApp(this.appId, this.file);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppsProvider with ChangeNotifier {
 | 
					class AppsProvider with ChangeNotifier {
 | 
				
			||||||
@@ -38,24 +43,24 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Variables to keep track of the app foreground status (installs can't run in the background)
 | 
					  // Variables to keep track of the app foreground status (installs can't run in the background)
 | 
				
			||||||
  bool isForeground = true;
 | 
					  bool isForeground = true;
 | 
				
			||||||
  late Stream<FGBGType> foregroundStream;
 | 
					  late Stream<FGBGType>? foregroundStream;
 | 
				
			||||||
  late StreamSubscription<FGBGType> foregroundSubscription;
 | 
					  late StreamSubscription<FGBGType>? foregroundSubscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  AppsProvider(
 | 
					  AppsProvider(
 | 
				
			||||||
      {bool shouldLoadApps = false,
 | 
					      {bool shouldLoadApps = false,
 | 
				
			||||||
      bool shouldCheckUpdatesAfterLoad = false,
 | 
					      bool shouldCheckUpdatesAfterLoad = false,
 | 
				
			||||||
      bool shouldDeleteAPKs = false}) {
 | 
					      bool shouldDeleteAPKs = false}) {
 | 
				
			||||||
    // Subscribe to changes in the app foreground status
 | 
					 | 
				
			||||||
    foregroundStream = FGBGEvents.stream.asBroadcastStream();
 | 
					 | 
				
			||||||
    foregroundSubscription = foregroundStream.listen((event) async {
 | 
					 | 
				
			||||||
      isForeground = event == FGBGType.foreground;
 | 
					 | 
				
			||||||
      if (isForeground) await loadApps();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (shouldDeleteAPKs) {
 | 
					 | 
				
			||||||
      deleteSavedAPKs();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (shouldLoadApps) {
 | 
					    if (shouldLoadApps) {
 | 
				
			||||||
 | 
					      // Subscribe to changes in the app foreground status
 | 
				
			||||||
 | 
					      foregroundStream = FGBGEvents.stream.asBroadcastStream();
 | 
				
			||||||
 | 
					      foregroundSubscription = foregroundStream?.listen((event) async {
 | 
				
			||||||
 | 
					        isForeground = event == FGBGType.foreground;
 | 
				
			||||||
 | 
					        if (isForeground) await loadApps();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      loadApps().then((_) {
 | 
					      loadApps().then((_) {
 | 
				
			||||||
 | 
					        if (shouldDeleteAPKs) {
 | 
				
			||||||
 | 
					          deleteSavedAPKs();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (shouldCheckUpdatesAfterLoad) {
 | 
					        if (shouldCheckUpdatesAfterLoad) {
 | 
				
			||||||
          checkUpdates();
 | 
					          checkUpdates();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -63,35 +68,85 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<ApkFile> downloadApp(String apkUrl, String appId) async {
 | 
					  downloadApk(String apkUrl, String fileName, Function? onProgress,
 | 
				
			||||||
 | 
					      Function? urlModifier,
 | 
				
			||||||
 | 
					      {bool useExistingIfExists = true}) async {
 | 
				
			||||||
 | 
					    var destDir = (await getExternalStorageDirectory())!.path;
 | 
				
			||||||
 | 
					    if (urlModifier != null) {
 | 
				
			||||||
 | 
					      apkUrl = await urlModifier(apkUrl);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    StreamedResponse response =
 | 
					    StreamedResponse response =
 | 
				
			||||||
        await Client().send(Request('GET', Uri.parse(apkUrl)));
 | 
					        await Client().send(Request('GET', Uri.parse(apkUrl)));
 | 
				
			||||||
    File downloadFile =
 | 
					    File downloadFile = File('$destDir/$fileName.apk');
 | 
				
			||||||
        File('${(await getExternalStorageDirectory())!.path}/$appId.apk');
 | 
					    var alreadyExists = downloadFile.existsSync();
 | 
				
			||||||
    if (downloadFile.existsSync()) {
 | 
					    if (!alreadyExists || !useExistingIfExists) {
 | 
				
			||||||
      downloadFile.deleteSync();
 | 
					      if (alreadyExists) {
 | 
				
			||||||
    }
 | 
					        downloadFile.deleteSync();
 | 
				
			||||||
    var length = response.contentLength;
 | 
					      }
 | 
				
			||||||
    var received = 0;
 | 
					 | 
				
			||||||
    var sink = downloadFile.openWrite();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await response.stream.map((s) {
 | 
					      var length = response.contentLength;
 | 
				
			||||||
      received += s.length;
 | 
					      var received = 0;
 | 
				
			||||||
      apps[appId]!.downloadProgress =
 | 
					      double? progress;
 | 
				
			||||||
          (length != null ? received / length * 100 : 30);
 | 
					      var sink = downloadFile.openWrite();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await response.stream.map((s) {
 | 
				
			||||||
 | 
					        received += s.length;
 | 
				
			||||||
 | 
					        progress = (length != null ? received / length * 100 : 30);
 | 
				
			||||||
 | 
					        if (onProgress != null) {
 | 
				
			||||||
 | 
					          onProgress(progress);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return s;
 | 
				
			||||||
 | 
					      }).pipe(sink);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sink.close();
 | 
				
			||||||
 | 
					      progress = null;
 | 
				
			||||||
 | 
					      if (onProgress != null) {
 | 
				
			||||||
 | 
					        onProgress(progress);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (response.statusCode != 200) {
 | 
				
			||||||
 | 
					        downloadFile.deleteSync();
 | 
				
			||||||
 | 
					        throw response.reasonPhrase ?? 'Unknown Error';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return downloadFile;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Downloads the App (preferred URL) and returns an ApkFile object
 | 
				
			||||||
 | 
					  // If the app was already saved, updates it's download progress % in memory
 | 
				
			||||||
 | 
					  // But also works for Apps that are not saved
 | 
				
			||||||
 | 
					  Future<DownloadedApp> downloadApp(App app) async {
 | 
				
			||||||
 | 
					    var fileName = '${app.id}-${app.latestVersion}-${app.preferredApkIndex}';
 | 
				
			||||||
 | 
					    File downloadFile = await downloadApk(app.apkUrls[app.preferredApkIndex],
 | 
				
			||||||
 | 
					        '${app.id}-${app.latestVersion}-${app.preferredApkIndex}',
 | 
				
			||||||
 | 
					        (double? progress) {
 | 
				
			||||||
 | 
					      if (apps[app.id] != null) {
 | 
				
			||||||
 | 
					        apps[app.id]!.downloadProgress = progress;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      notifyListeners();
 | 
					      notifyListeners();
 | 
				
			||||||
      return s;
 | 
					    }, SourceProvider().getSource(app.url).apkUrlPrefetchModifier);
 | 
				
			||||||
    }).pipe(sink);
 | 
					    // Delete older versions of the APK if any
 | 
				
			||||||
 | 
					    for (var file in downloadFile.parent.listSync()) {
 | 
				
			||||||
    await sink.close();
 | 
					      var fn = file.path.split('/').last;
 | 
				
			||||||
    apps[appId]!.downloadProgress = null;
 | 
					      if (fn.startsWith('${app.id}-') &&
 | 
				
			||||||
    notifyListeners();
 | 
					          fn.endsWith('.apk') &&
 | 
				
			||||||
 | 
					          fn != '$fileName.apk') {
 | 
				
			||||||
    if (response.statusCode != 200) {
 | 
					        file.delete();
 | 
				
			||||||
      downloadFile.deleteSync();
 | 
					      }
 | 
				
			||||||
      throw response.reasonPhrase ?? 'Unknown Error';
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return ApkFile(appId, downloadFile);
 | 
					    // If the ID has changed (as it should on first download), replace it
 | 
				
			||||||
 | 
					    var newInfo = await PackageArchiveInfo.fromPath(downloadFile.path);
 | 
				
			||||||
 | 
					    if (app.id != newInfo.packageName) {
 | 
				
			||||||
 | 
					      var originalAppId = app.id;
 | 
				
			||||||
 | 
					      app.id = newInfo.packageName;
 | 
				
			||||||
 | 
					      downloadFile = downloadFile.renameSync(
 | 
				
			||||||
 | 
					          '${downloadFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
 | 
				
			||||||
 | 
					      if (apps[originalAppId] != null) {
 | 
				
			||||||
 | 
					        await removeApps([originalAppId]);
 | 
				
			||||||
 | 
					        await saveApps([app]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return DownloadedApp(app.id, downloadFile);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool areDownloadsRunning() => apps.values
 | 
					  bool areDownloadsRunning() => apps.values
 | 
				
			||||||
@@ -102,8 +157,8 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    // TODO: This is unreliable - try to get from OS in the future
 | 
					    // TODO: This is unreliable - try to get from OS in the future
 | 
				
			||||||
    var osInfo = await DeviceInfoPlugin().androidInfo;
 | 
					    var osInfo = await DeviceInfoPlugin().androidInfo;
 | 
				
			||||||
    return app.installedVersion != null &&
 | 
					    return app.installedVersion != null &&
 | 
				
			||||||
        osInfo.version.sdkInt! >= 30 &&
 | 
					        osInfo.version.sdkInt >= 30 &&
 | 
				
			||||||
        osInfo.version.release!.compareTo('12') >= 0;
 | 
					        osInfo.version.release.compareTo('12') >= 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> askUserToReturnToForeground(BuildContext context,
 | 
					  Future<void> askUserToReturnToForeground(BuildContext context,
 | 
				
			||||||
@@ -124,11 +179,62 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
  // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
 | 
					  // So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
 | 
				
			||||||
  // If appropriate criteria are met, the update (never a fresh install) happens silently  in the background
 | 
					  // If appropriate criteria are met, the update (never a fresh install) happens silently  in the background
 | 
				
			||||||
  // But even then, we don't know if it actually succeeded
 | 
					  // But even then, we don't know if it actually succeeded
 | 
				
			||||||
  Future<void> installApk(ApkFile file) async {
 | 
					  Future<void> installApk(DownloadedApp file) async {
 | 
				
			||||||
    await AppInstaller.installApk(file.file.path, actionRequired: false);
 | 
					    var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
 | 
				
			||||||
 | 
					    AppInfo? appInfo;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      // OK
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (appInfo != null &&
 | 
				
			||||||
 | 
					        int.parse(newInfo.buildNumber) < appInfo.versionCode!) {
 | 
				
			||||||
 | 
					      throw 'Can\'t install an older version';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (appInfo == null ||
 | 
				
			||||||
 | 
					        int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
 | 
				
			||||||
 | 
					      await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    apps[file.appId]!.app.installedVersion =
 | 
					    apps[file.appId]!.app.installedVersion =
 | 
				
			||||||
        apps[file.appId]!.app.latestVersion;
 | 
					        apps[file.appId]!.app.latestVersion;
 | 
				
			||||||
    await saveApps([apps[file.appId]!.app]);
 | 
					    // Don't correct install status as installation may not be done yet
 | 
				
			||||||
 | 
					    await saveApps([apps[file.appId]!.app], shouldCorrectInstallStatus: false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<String?> selectApkUrl(App app, BuildContext? context) async {
 | 
				
			||||||
 | 
					    // If the App has more than one APK, the user should pick one (if context provided)
 | 
				
			||||||
 | 
					    String? apkUrl = app.apkUrls[app.preferredApkIndex];
 | 
				
			||||||
 | 
					    if (app.apkUrls.length > 1 && context != null) {
 | 
				
			||||||
 | 
					      apkUrl = await showDialog(
 | 
				
			||||||
 | 
					          context: context,
 | 
				
			||||||
 | 
					          builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					            return APKPicker(app: app, initVal: apkUrl);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
 | 
				
			||||||
 | 
					    if (apkUrl != null &&
 | 
				
			||||||
 | 
					        Uri.parse(apkUrl).origin != Uri.parse(app.url).origin &&
 | 
				
			||||||
 | 
					        context != null) {
 | 
				
			||||||
 | 
					      if (await showDialog(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					                return APKOriginWarningDialog(
 | 
				
			||||||
 | 
					                    sourceUrl: app.url, apkUrl: apkUrl!);
 | 
				
			||||||
 | 
					              }) !=
 | 
				
			||||||
 | 
					          true) {
 | 
				
			||||||
 | 
					        apkUrl = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return apkUrl;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, List<String>> addToErrorMap(
 | 
				
			||||||
 | 
					      Map<String, List<String>> errors, String appId, String error) {
 | 
				
			||||||
 | 
					    var tempIds = errors.remove(error);
 | 
				
			||||||
 | 
					    tempIds ??= [];
 | 
				
			||||||
 | 
					    tempIds.add(appId);
 | 
				
			||||||
 | 
					    errors.putIfAbsent(error, () => tempIds!);
 | 
				
			||||||
 | 
					    return errors;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Given a list of AppIds, uses stored info about the apps to download APKs and install them
 | 
					  // Given a list of AppIds, uses stored info about the apps to download APKs and install them
 | 
				
			||||||
@@ -136,37 +242,16 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
  // If no BuildContext is provided, apps that require user interaction are ignored
 | 
					  // If no BuildContext is provided, apps that require user interaction are ignored
 | 
				
			||||||
  // If user input is needed and the App is in the background, a notification is sent to get the user's attention
 | 
					  // If user input is needed and the App is in the background, a notification is sent to get the user's attention
 | 
				
			||||||
  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
					  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
				
			||||||
  Future<List<String>> downloadAndInstallLatestApp(
 | 
					  Future<List<String>> downloadAndInstallLatestApps(
 | 
				
			||||||
      List<String> appIds, BuildContext? context) async {
 | 
					      List<String> appIds, BuildContext? context) async {
 | 
				
			||||||
    Map<String, String> appsToInstall = {};
 | 
					    List<String> appsToInstall = [];
 | 
				
			||||||
    for (var id in appIds) {
 | 
					    for (var id in appIds) {
 | 
				
			||||||
      if (apps[id] == null) {
 | 
					      if (apps[id] == null) {
 | 
				
			||||||
        throw 'App not found';
 | 
					        throw 'App not found';
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // If the App has more than one APK, the user should pick one (if context provided)
 | 
					      String? apkUrl = await selectApkUrl(apps[id]!.app, context);
 | 
				
			||||||
      String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
 | 
					
 | 
				
			||||||
      if (apps[id]!.app.apkUrls.length > 1 && context != null) {
 | 
					 | 
				
			||||||
        apkUrl = await showDialog(
 | 
					 | 
				
			||||||
            context: context,
 | 
					 | 
				
			||||||
            builder: (BuildContext ctx) {
 | 
					 | 
				
			||||||
              return APKPicker(app: apps[id]!.app, initVal: apkUrl);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
 | 
					 | 
				
			||||||
      if (apkUrl != null &&
 | 
					 | 
				
			||||||
          Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin &&
 | 
					 | 
				
			||||||
          context != null) {
 | 
					 | 
				
			||||||
        if (await showDialog(
 | 
					 | 
				
			||||||
                context: context,
 | 
					 | 
				
			||||||
                builder: (BuildContext ctx) {
 | 
					 | 
				
			||||||
                  return APKOriginWarningDialog(
 | 
					 | 
				
			||||||
                      sourceUrl: apps[id]!.app.url, apkUrl: apkUrl!);
 | 
					 | 
				
			||||||
                }) !=
 | 
					 | 
				
			||||||
            true) {
 | 
					 | 
				
			||||||
          apkUrl = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (apkUrl != null) {
 | 
					      if (apkUrl != null) {
 | 
				
			||||||
        int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
 | 
					        int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
 | 
				
			||||||
        if (urlInd != apps[id]!.app.preferredApkIndex) {
 | 
					        if (urlInd != apps[id]!.app.preferredApkIndex) {
 | 
				
			||||||
@@ -176,18 +261,28 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
        if (context != null ||
 | 
					        if (context != null ||
 | 
				
			||||||
            (await canInstallSilently(apps[id]!.app) &&
 | 
					            (await canInstallSilently(apps[id]!.app) &&
 | 
				
			||||||
                apps[id]!.app.apkUrls.length == 1)) {
 | 
					                apps[id]!.app.apkUrls.length == 1)) {
 | 
				
			||||||
          appsToInstall.putIfAbsent(id, () => apkUrl!);
 | 
					          appsToInstall.add(id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    Map<String, List<String>> errors = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
 | 
					    List<DownloadedApp?> downloadedFiles =
 | 
				
			||||||
        .map((entry) => downloadApp(entry.value, entry.key)));
 | 
					        await Future.wait(appsToInstall.map((id) async {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return await downloadApp(apps[id]!.app);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        addToErrorMap(errors, id, e.toString());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					    downloadedFiles =
 | 
				
			||||||
 | 
					        downloadedFiles.where((element) => element != null).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<ApkFile> silentUpdates = [];
 | 
					    List<DownloadedApp> silentUpdates = [];
 | 
				
			||||||
    List<ApkFile> regularInstalls = [];
 | 
					    List<DownloadedApp> regularInstalls = [];
 | 
				
			||||||
    for (var f in downloadedFiles) {
 | 
					    for (var f in downloadedFiles) {
 | 
				
			||||||
      bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
 | 
					      bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
 | 
				
			||||||
      if (willBeSilent) {
 | 
					      if (willBeSilent) {
 | 
				
			||||||
        silentUpdates.add(f);
 | 
					        silentUpdates.add(f);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@@ -196,9 +291,9 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If Obtainium is being installed, it should be the last one
 | 
					    // If Obtainium is being installed, it should be the last one
 | 
				
			||||||
    List<ApkFile> moveObtainiumToEnd(List<ApkFile> items) {
 | 
					    List<DownloadedApp> moveObtainiumToEnd(List<DownloadedApp> items) {
 | 
				
			||||||
      String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
 | 
					      String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
 | 
				
			||||||
      ApkFile? temp;
 | 
					      DownloadedApp? temp;
 | 
				
			||||||
      items.removeWhere((element) {
 | 
					      items.removeWhere((element) {
 | 
				
			||||||
        bool res = element.appId == obtainiumId;
 | 
					        bool res = element.appId == obtainiumId;
 | 
				
			||||||
        if (res) {
 | 
					        if (res) {
 | 
				
			||||||
@@ -212,24 +307,40 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
      return items;
 | 
					      return items;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO: Remove below line if silentupdates are ever figured out
 | 
				
			||||||
 | 
					    regularInstalls.addAll(silentUpdates);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    silentUpdates = moveObtainiumToEnd(silentUpdates);
 | 
					    silentUpdates = moveObtainiumToEnd(silentUpdates);
 | 
				
			||||||
    regularInstalls = moveObtainiumToEnd(regularInstalls);
 | 
					    regularInstalls = moveObtainiumToEnd(regularInstalls);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (var u in silentUpdates) {
 | 
					    // TODO: Uncomment below if silentupdates are ever figured out
 | 
				
			||||||
      await installApk(u);
 | 
					    // for (var u in silentUpdates) {
 | 
				
			||||||
    }
 | 
					    //   await installApk(u, silent: true); // Would need to add silent option
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (context != null) {
 | 
					    if (context != null) {
 | 
				
			||||||
      if (regularInstalls.isNotEmpty) {
 | 
					      if (regularInstalls.isNotEmpty) {
 | 
				
			||||||
        // ignore: use_build_context_synchronously
 | 
					        // ignore: use_build_context_synchronously
 | 
				
			||||||
        await askUserToReturnToForeground(context);
 | 
					        await askUserToReturnToForeground(context, waitForFG: true);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      for (var i in regularInstalls) {
 | 
					      for (var i in regularInstalls) {
 | 
				
			||||||
        await installApk(i);
 | 
					        try {
 | 
				
			||||||
 | 
					          await installApk(i);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          addToErrorMap(errors, i.appId, e.toString());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (errors.isNotEmpty) {
 | 
				
			||||||
 | 
					      String finalError = '';
 | 
				
			||||||
 | 
					      for (var e in errors.keys) {
 | 
				
			||||||
 | 
					        finalError +=
 | 
				
			||||||
 | 
					            '$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      throw finalError;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return downloadedFiles.map((e) => e.appId).toList();
 | 
					    return downloadedFiles.map((e) => e!.appId).toList();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<Directory> getAppsDir() async {
 | 
					  Future<Directory> getAppsDir() async {
 | 
				
			||||||
@@ -241,16 +352,83 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    return appsDir;
 | 
					    return appsDir;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Delete all stored APKs except those likely to still be needed
 | 
				
			||||||
  Future<void> deleteSavedAPKs() async {
 | 
					  Future<void> deleteSavedAPKs() async {
 | 
				
			||||||
    (await getExternalStorageDirectory())
 | 
					    List<FileSystemEntity>? apks = (await getExternalStorageDirectory())
 | 
				
			||||||
        ?.listSync()
 | 
					        ?.listSync()
 | 
				
			||||||
        .where((element) => element.path.endsWith('.apk'))
 | 
					        .where((element) => element.path.endsWith('.apk'))
 | 
				
			||||||
        .forEach((element) {
 | 
					        .toList();
 | 
				
			||||||
      element.deleteSync();
 | 
					    if (apks != null && apks.isNotEmpty) {
 | 
				
			||||||
    });
 | 
					      for (var apk in apks) {
 | 
				
			||||||
 | 
					        var shouldDelete = true;
 | 
				
			||||||
 | 
					        var temp = apk.path.split('/').last;
 | 
				
			||||||
 | 
					        temp = temp.substring(0, temp.length - 4);
 | 
				
			||||||
 | 
					        var fn = temp.split('-');
 | 
				
			||||||
 | 
					        if (fn.length == 3) {
 | 
				
			||||||
 | 
					          var possibleId = fn[0];
 | 
				
			||||||
 | 
					          var possibleVersion = fn[1];
 | 
				
			||||||
 | 
					          var possibleApkUrlIndex = fn[2];
 | 
				
			||||||
 | 
					          if (apps[possibleId] != null) {
 | 
				
			||||||
 | 
					            if (apps[possibleId] != null &&
 | 
				
			||||||
 | 
					                apps[possibleId]?.app != null &&
 | 
				
			||||||
 | 
					                apps[possibleId]!.app.installedVersion !=
 | 
				
			||||||
 | 
					                    apps[possibleId]!.app.latestVersion &&
 | 
				
			||||||
 | 
					                apps[possibleId]!.app.latestVersion == possibleVersion &&
 | 
				
			||||||
 | 
					                apps[possibleId]!.app.preferredApkIndex.toString() ==
 | 
				
			||||||
 | 
					                    possibleApkUrlIndex) {
 | 
				
			||||||
 | 
					              shouldDelete = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (shouldDelete) apk.delete();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> loadApps() async {
 | 
					  Future<AppInfo?> getInstalledInfo(String? packageName) async {
 | 
				
			||||||
 | 
					    if (packageName != null) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return await InstalledApps.getAppInfo(packageName);
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        // OK
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String standardizeVersionString(String versionString) {
 | 
				
			||||||
 | 
					    return versionString.characters
 | 
				
			||||||
 | 
					        .where((p0) => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']
 | 
				
			||||||
 | 
					            .contains(p0))
 | 
				
			||||||
 | 
					        .join('');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // If the App says it is installed by installedInfo is null, set it to not installed
 | 
				
			||||||
 | 
					  // If the App says is is not installed but installedInfo exists, try to set it to installed as latest version...
 | 
				
			||||||
 | 
					  // ...if the latestVersion seems to match the version in installedInfo (not guaranteed)
 | 
				
			||||||
 | 
					  App? correctInstallStatus(App app, AppInfo? installedInfo) {
 | 
				
			||||||
 | 
					    var modded = false;
 | 
				
			||||||
 | 
					    if (installedInfo == null && app.installedVersion != null) {
 | 
				
			||||||
 | 
					      app.installedVersion = null;
 | 
				
			||||||
 | 
					      modded = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (installedInfo != null && app.installedVersion == null) {
 | 
				
			||||||
 | 
					      if (standardizeVersionString(app.latestVersion) ==
 | 
				
			||||||
 | 
					          installedInfo.versionName) {
 | 
				
			||||||
 | 
					        app.installedVersion = app.latestVersion;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        app.installedVersion = installedInfo.versionName;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      modded = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return modded ? app : null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> loadApps({shouldCorrectInstallStatus = true}) async {
 | 
				
			||||||
 | 
					    while (loadingApps) {
 | 
				
			||||||
 | 
					      await Future.delayed(const Duration(microseconds: 1));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    loadingApps = true;
 | 
					    loadingApps = true;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
    List<FileSystemEntity> appFiles = (await getAppsDir())
 | 
					    List<FileSystemEntity> appFiles = (await getAppsDir())
 | 
				
			||||||
@@ -258,22 +436,54 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
        .where((item) => item.path.toLowerCase().endsWith('.json'))
 | 
					        .where((item) => item.path.toLowerCase().endsWith('.json'))
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
    apps.clear();
 | 
					    apps.clear();
 | 
				
			||||||
 | 
					    var sp = SourceProvider();
 | 
				
			||||||
 | 
					    List<List<String>> errors = [];
 | 
				
			||||||
    for (int i = 0; i < appFiles.length; i++) {
 | 
					    for (int i = 0; i < appFiles.length; i++) {
 | 
				
			||||||
      App app =
 | 
					      App app =
 | 
				
			||||||
          App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
 | 
					          App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
 | 
				
			||||||
      apps.putIfAbsent(app.id, () => AppInMemory(app, null));
 | 
					      var info = await getInstalledInfo(app.id);
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        sp.getSource(app.url);
 | 
				
			||||||
 | 
					        apps.putIfAbsent(app.id, () => AppInMemory(app, null, info));
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        errors.add([app.id, app.name, e.toString()]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (errors.isNotEmpty) {
 | 
				
			||||||
 | 
					      removeApps(errors.map((e) => e[0]).toList());
 | 
				
			||||||
 | 
					      NotificationsProvider().notify(
 | 
				
			||||||
 | 
					          AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    loadingApps = false;
 | 
					    loadingApps = false;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					    // For any that are not installed (by ID == package name), set to not installed if needed
 | 
				
			||||||
 | 
					    if (shouldCorrectInstallStatus) {
 | 
				
			||||||
 | 
					      List<App> modifiedApps = [];
 | 
				
			||||||
 | 
					      for (var app in apps.values) {
 | 
				
			||||||
 | 
					        var moddedApp = correctInstallStatus(app.app, app.installedInfo);
 | 
				
			||||||
 | 
					        if (moddedApp != null) {
 | 
				
			||||||
 | 
					          modifiedApps.add(moddedApp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (modifiedApps.isNotEmpty) {
 | 
				
			||||||
 | 
					        await saveApps(modifiedApps, shouldCorrectInstallStatus: false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> saveApps(List<App> apps) async {
 | 
					  Future<void> saveApps(List<App> apps,
 | 
				
			||||||
 | 
					      {bool shouldCorrectInstallStatus = true}) async {
 | 
				
			||||||
    for (var app in apps) {
 | 
					    for (var app in apps) {
 | 
				
			||||||
 | 
					      AppInfo? info = await getInstalledInfo(app.id);
 | 
				
			||||||
 | 
					      app.name = info?.name ?? app.name;
 | 
				
			||||||
 | 
					      if (shouldCorrectInstallStatus) {
 | 
				
			||||||
 | 
					        app = correctInstallStatus(app, info) ?? app;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      File('${(await getAppsDir()).path}/${app.id}.json')
 | 
					      File('${(await getAppsDir()).path}/${app.id}.json')
 | 
				
			||||||
          .writeAsStringSync(jsonEncode(app.toJson()));
 | 
					          .writeAsStringSync(jsonEncode(app.toJson()));
 | 
				
			||||||
      this.apps.update(
 | 
					      this.apps.update(
 | 
				
			||||||
          app.id, (value) => AppInMemory(app, value.downloadProgress),
 | 
					          app.id, (value) => AppInMemory(app, value.downloadProgress, info),
 | 
				
			||||||
          ifAbsent: () => AppInMemory(app, null));
 | 
					          ifAbsent: () => AppInMemory(app, null, info));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -300,53 +510,81 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    return app.latestVersion != apps[app.id]?.app.installedVersion;
 | 
					    return app.latestVersion != apps[app.id]?.app.installedVersion;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<App?> getUpdate(String appId) async {
 | 
					  Future<App?> getUpdate(String appId,
 | 
				
			||||||
 | 
					      {bool shouldCorrectInstallStatus = true}) async {
 | 
				
			||||||
    App? currentApp = apps[appId]!.app;
 | 
					    App? currentApp = apps[appId]!.app;
 | 
				
			||||||
    SourceProvider sourceProvider = SourceProvider();
 | 
					    SourceProvider sourceProvider = SourceProvider();
 | 
				
			||||||
    App newApp = await sourceProvider.getApp(
 | 
					    App newApp = await sourceProvider.getApp(
 | 
				
			||||||
        sourceProvider.getSource(currentApp.url),
 | 
					        sourceProvider.getSource(currentApp.url),
 | 
				
			||||||
        currentApp.url,
 | 
					        currentApp.url,
 | 
				
			||||||
        currentApp.additionalData);
 | 
					        currentApp.additionalData,
 | 
				
			||||||
    if (newApp.latestVersion != currentApp.latestVersion) {
 | 
					        name: currentApp.name,
 | 
				
			||||||
      newApp.installedVersion = currentApp.installedVersion;
 | 
					        id: currentApp.id);
 | 
				
			||||||
      if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
 | 
					    newApp.installedVersion = currentApp.installedVersion;
 | 
				
			||||||
        newApp.preferredApkIndex = currentApp.preferredApkIndex;
 | 
					    if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
 | 
				
			||||||
      }
 | 
					      newApp.preferredApkIndex = currentApp.preferredApkIndex;
 | 
				
			||||||
      await saveApps([newApp]);
 | 
					 | 
				
			||||||
      return newApp;
 | 
					 | 
				
			||||||
    } else if ((newApp.lastUpdateCheck?.microsecondsSinceEpoch ?? 0) -
 | 
					 | 
				
			||||||
            (currentApp.lastUpdateCheck?.microsecondsSinceEpoch ?? 0) >
 | 
					 | 
				
			||||||
        5000000) {
 | 
					 | 
				
			||||||
      currentApp.lastUpdateCheck = newApp.lastUpdateCheck;
 | 
					 | 
				
			||||||
      await saveApps([currentApp]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    await saveApps([newApp],
 | 
				
			||||||
 | 
					        shouldCorrectInstallStatus: shouldCorrectInstallStatus);
 | 
				
			||||||
 | 
					    return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<List<App>> checkUpdates({DateTime? ignoreAfter}) async {
 | 
					  Future<List<App>> checkUpdates(
 | 
				
			||||||
 | 
					      {DateTime? ignoreAfter,
 | 
				
			||||||
 | 
					      bool immediatelyThrowRateLimitError = false,
 | 
				
			||||||
 | 
					      bool shouldCorrectInstallStatus = true,
 | 
				
			||||||
 | 
					      bool immediatelyThrowSocketError = false}) async {
 | 
				
			||||||
    List<App> updates = [];
 | 
					    List<App> updates = [];
 | 
				
			||||||
 | 
					    Map<String, List<String>> errors = {};
 | 
				
			||||||
    if (!gettingUpdates) {
 | 
					    if (!gettingUpdates) {
 | 
				
			||||||
      gettingUpdates = true;
 | 
					      gettingUpdates = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      List<String> appIds = apps.keys.toList();
 | 
					      try {
 | 
				
			||||||
      if (ignoreAfter != null) {
 | 
					        List<String> appIds = apps.keys.toList();
 | 
				
			||||||
        appIds = appIds
 | 
					        if (ignoreAfter != null) {
 | 
				
			||||||
            .where((id) =>
 | 
					          appIds = appIds
 | 
				
			||||||
                apps[id]!.app.lastUpdateCheck == null ||
 | 
					              .where((id) =>
 | 
				
			||||||
                apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
 | 
					                  apps[id]!.app.lastUpdateCheck == null ||
 | 
				
			||||||
            .toList();
 | 
					                  apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
 | 
				
			||||||
      }
 | 
					              .toList();
 | 
				
			||||||
      appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
 | 
					 | 
				
			||||||
              DateTime.fromMicrosecondsSinceEpoch(0))
 | 
					 | 
				
			||||||
          .compareTo(apps[b]!.app.lastUpdateCheck ??
 | 
					 | 
				
			||||||
              DateTime.fromMicrosecondsSinceEpoch(0)));
 | 
					 | 
				
			||||||
      for (int i = 0; i < appIds.length; i++) {
 | 
					 | 
				
			||||||
        App? newApp = await getUpdate(appIds[i]);
 | 
					 | 
				
			||||||
        if (newApp != null) {
 | 
					 | 
				
			||||||
          updates.add(newApp);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
 | 
				
			||||||
 | 
					                DateTime.fromMicrosecondsSinceEpoch(0))
 | 
				
			||||||
 | 
					            .compareTo(apps[b]!.app.lastUpdateCheck ??
 | 
				
			||||||
 | 
					                DateTime.fromMicrosecondsSinceEpoch(0)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (int i = 0; i < appIds.length; i++) {
 | 
				
			||||||
 | 
					          App? newApp;
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            newApp = await getUpdate(appIds[i],
 | 
				
			||||||
 | 
					                shouldCorrectInstallStatus: shouldCorrectInstallStatus);
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            if (e is RateLimitError && immediatelyThrowRateLimitError) {
 | 
				
			||||||
 | 
					              rethrow;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (e is SocketException && immediatelyThrowSocketError) {
 | 
				
			||||||
 | 
					              rethrow;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            var tempIds = errors.remove(e.toString());
 | 
				
			||||||
 | 
					            tempIds ??= [];
 | 
				
			||||||
 | 
					            tempIds.add(appIds[i]);
 | 
				
			||||||
 | 
					            errors.putIfAbsent(e.toString(), () => tempIds!);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (newApp != null) {
 | 
				
			||||||
 | 
					            updates.add(newApp);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        gettingUpdates = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      gettingUpdates = false;
 | 
					    }
 | 
				
			||||||
 | 
					    if (errors.isNotEmpty) {
 | 
				
			||||||
 | 
					      String finalError = '';
 | 
				
			||||||
 | 
					      for (var e in errors.keys) {
 | 
				
			||||||
 | 
					        finalError +=
 | 
				
			||||||
 | 
					            '$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      throw finalError;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return updates;
 | 
					    return updates;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -389,18 +627,22 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
 | 
					    List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
 | 
				
			||||||
        .map((e) => App.fromJson(e))
 | 
					        .map((e) => App.fromJson(e))
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
    for (App a in importedApps) {
 | 
					    while (loadingApps) {
 | 
				
			||||||
      a.installedVersion =
 | 
					      await Future.delayed(const Duration(microseconds: 1));
 | 
				
			||||||
          apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null;
 | 
					 | 
				
			||||||
      await saveApps([a]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    for (App a in importedApps) {
 | 
				
			||||||
 | 
					      if (apps[a.id]?.app.installedVersion != null) {
 | 
				
			||||||
 | 
					        a.installedVersion = apps[a.id]?.app.installedVersion;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await saveApps(importedApps);
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
    return importedApps.length;
 | 
					    return importedApps.length;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    foregroundSubscription.cancel();
 | 
					    foregroundSubscription?.cancel();
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -428,7 +670,10 @@ class _APKPickerState extends State<APKPicker> {
 | 
				
			|||||||
        Text('${widget.app.name} has more than one package:'),
 | 
					        Text('${widget.app.name} has more than one package:'),
 | 
				
			||||||
        const SizedBox(height: 16),
 | 
					        const SizedBox(height: 16),
 | 
				
			||||||
        ...widget.app.apkUrls.map((u) => RadioListTile<String>(
 | 
					        ...widget.app.apkUrls.map((u) => RadioListTile<String>(
 | 
				
			||||||
            title: Text(Uri.parse(u).pathSegments.last),
 | 
					            title: Text(Uri.parse(u)
 | 
				
			||||||
 | 
					                .pathSegments
 | 
				
			||||||
 | 
					                .where((element) => element.isNotEmpty)
 | 
				
			||||||
 | 
					                .last),
 | 
				
			||||||
            value: u,
 | 
					            value: u,
 | 
				
			||||||
            groupValue: apkUrl,
 | 
					            groupValue: apkUrl,
 | 
				
			||||||
            onChanged: (String? val) {
 | 
					            onChanged: (String? val) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,6 +61,24 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
 | 
				
			|||||||
            Importance.high);
 | 
					            Importance.high);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AppsRemovedNotification extends ObtainiumNotification {
 | 
				
			||||||
 | 
					  AppsRemovedNotification(List<List<String>> namedReasons)
 | 
				
			||||||
 | 
					      : super(
 | 
				
			||||||
 | 
					            6,
 | 
				
			||||||
 | 
					            'Apps Removed',
 | 
				
			||||||
 | 
					            '',
 | 
				
			||||||
 | 
					            'APPS_REMOVED',
 | 
				
			||||||
 | 
					            'Apps Removed',
 | 
				
			||||||
 | 
					            'Notifies the user that one or more Apps were removed due to errors while loading them',
 | 
				
			||||||
 | 
					            Importance.max) {
 | 
				
			||||||
 | 
					    message = '';
 | 
				
			||||||
 | 
					    for (var r in namedReasons) {
 | 
				
			||||||
 | 
					      message += '${r[0]} was removed due to this error: ${r[1]}. \n';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    message = message.trim();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final completeInstallationNotification = ObtainiumNotification(
 | 
					final completeInstallationNotification = ObtainiumNotification(
 | 
				
			||||||
    1,
 | 
					    1,
 | 
				
			||||||
    'Complete App Installation',
 | 
					    'Complete App Installation',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,8 +74,8 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SortColumnSettings get sortColumn {
 | 
					  SortColumnSettings get sortColumn {
 | 
				
			||||||
    return SortColumnSettings
 | 
					    return SortColumnSettings.values[
 | 
				
			||||||
        .values[prefs?.getInt('sortColumn') ?? SortColumnSettings.added.index];
 | 
					        prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set sortColumn(SortColumnSettings s) {
 | 
					  set sortColumn(SortColumnSettings s) {
 | 
				
			||||||
@@ -85,7 +85,7 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  SortOrderSettings get sortOrder {
 | 
					  SortOrderSettings get sortOrder {
 | 
				
			||||||
    return SortOrderSettings.values[
 | 
					    return SortOrderSettings.values[
 | 
				
			||||||
        prefs?.getInt('sortOrder') ?? SortOrderSettings.descending.index];
 | 
					        prefs?.getInt('sortOrder') ?? SortOrderSettings.ascending.index];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set sortOrder(SortOrderSettings s) {
 | 
					  set sortOrder(SortOrderSettings s) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,7 +53,7 @@ class App {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls';
 | 
					    return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory App.fromJson(Map<String, dynamic> json) => App(
 | 
					  factory App.fromJson(Map<String, dynamic> json) => App(
 | 
				
			||||||
@@ -104,6 +104,11 @@ preStandardizeUrl(String url) {
 | 
				
			|||||||
  if (url.toLowerCase().indexOf('https://www.') == 0) {
 | 
					  if (url.toLowerCase().indexOf('https://www.') == 0) {
 | 
				
			||||||
    url = 'https://${url.substring(12)}';
 | 
					    url = 'https://${url.substring(12)}';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  url = url
 | 
				
			||||||
 | 
					      .split('/')
 | 
				
			||||||
 | 
					      .where((e) => e.isNotEmpty)
 | 
				
			||||||
 | 
					      .join('/')
 | 
				
			||||||
 | 
					      .replaceFirst(':/', '://');
 | 
				
			||||||
  return url;
 | 
					  return url;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -136,6 +141,8 @@ abstract class AppSource {
 | 
				
			|||||||
  late List<List<GeneratedFormItem>> additionalDataFormItems;
 | 
					  late List<List<GeneratedFormItem>> additionalDataFormItems;
 | 
				
			||||||
  late List<String> additionalDataDefaults;
 | 
					  late List<String> additionalDataDefaults;
 | 
				
			||||||
  late List<GeneratedFormItem> moreSourceSettingsFormItems;
 | 
					  late List<GeneratedFormItem> moreSourceSettingsFormItems;
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl);
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class MassAppSource {
 | 
					abstract class MassAppSource {
 | 
				
			||||||
@@ -153,7 +160,8 @@ class SourceProvider {
 | 
				
			|||||||
    IzzyOnDroid(),
 | 
					    IzzyOnDroid(),
 | 
				
			||||||
    Mullvad(),
 | 
					    Mullvad(),
 | 
				
			||||||
    Signal(),
 | 
					    Signal(),
 | 
				
			||||||
    SourceForge()
 | 
					    SourceForge(),
 | 
				
			||||||
 | 
					    // APKMirror()
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add more mass source classes here so they are available via the service
 | 
					  // Add more mass source classes here so they are available via the service
 | 
				
			||||||
@@ -185,18 +193,21 @@ class SourceProvider {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String generateTempID(AppNames names, AppSource source) =>
 | 
				
			||||||
 | 
					      '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<App> getApp(AppSource source, String url, List<String> additionalData,
 | 
					  Future<App> getApp(AppSource source, String url, List<String> additionalData,
 | 
				
			||||||
      {String customName = ''}) async {
 | 
					      {String name = '', String? id}) async {
 | 
				
			||||||
    String standardUrl = source.standardizeURL(preStandardizeUrl(url));
 | 
					    String standardUrl = source.standardizeURL(preStandardizeUrl(url));
 | 
				
			||||||
    AppNames names = source.getAppNames(standardUrl);
 | 
					    AppNames names = source.getAppNames(standardUrl);
 | 
				
			||||||
    APKDetails apk =
 | 
					    APKDetails apk =
 | 
				
			||||||
        await source.getLatestAPKDetails(standardUrl, additionalData);
 | 
					        await source.getLatestAPKDetails(standardUrl, additionalData);
 | 
				
			||||||
    return App(
 | 
					    return App(
 | 
				
			||||||
        '${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
 | 
					        id ?? generateTempID(names, source),
 | 
				
			||||||
        standardUrl,
 | 
					        standardUrl,
 | 
				
			||||||
        names.author[0].toUpperCase() + names.author.substring(1),
 | 
					        names.author[0].toUpperCase() + names.author.substring(1),
 | 
				
			||||||
        customName.trim().isNotEmpty
 | 
					        name.trim().isNotEmpty
 | 
				
			||||||
            ? customName
 | 
					            ? name
 | 
				
			||||||
            : names.name[0].toUpperCase() + names.name.substring(1),
 | 
					            : names.name[0].toUpperCase() + names.name.substring(1),
 | 
				
			||||||
        null,
 | 
					        null,
 | 
				
			||||||
        apk.version,
 | 
					        apk.version,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										169
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						@@ -7,14 +7,14 @@ packages:
 | 
				
			|||||||
      name: animations
 | 
					      name: animations
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.5"
 | 
					    version: "2.0.7"
 | 
				
			||||||
  archive:
 | 
					  archive:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: archive
 | 
					      name: archive
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.3.1"
 | 
					    version: "3.3.2"
 | 
				
			||||||
  args:
 | 
					  args:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -71,6 +71,13 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.16.0"
 | 
					    version: "1.16.0"
 | 
				
			||||||
 | 
					  cross_file:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: cross_file
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.3.3+2"
 | 
				
			||||||
  crypto:
 | 
					  crypto:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -105,42 +112,14 @@ packages:
 | 
				
			|||||||
      name: device_info_plus
 | 
					      name: device_info_plus
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.1.2"
 | 
					    version: "8.0.0"
 | 
				
			||||||
  device_info_plus_linux:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: device_info_plus_linux
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.0"
 | 
					 | 
				
			||||||
  device_info_plus_macos:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: device_info_plus_macos
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.0"
 | 
					 | 
				
			||||||
  device_info_plus_platform_interface:
 | 
					  device_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: device_info_plus_platform_interface
 | 
					      name: device_info_plus_platform_interface
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.0"
 | 
					    version: "7.0.0"
 | 
				
			||||||
  device_info_plus_web:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: device_info_plus_web
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.0"
 | 
					 | 
				
			||||||
  device_info_plus_windows:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: device_info_plus_windows
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "4.1.0"
 | 
					 | 
				
			||||||
  dynamic_color:
 | 
					  dynamic_color:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -175,7 +154,7 @@ packages:
 | 
				
			|||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.2.0+1"
 | 
					    version: "5.2.2"
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -188,13 +167,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.2.0"
 | 
					    version: "0.2.0"
 | 
				
			||||||
  flutter_install_app:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: flutter_install_app
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "1.3.0"
 | 
					 | 
				
			||||||
  flutter_launcher_icons:
 | 
					  flutter_launcher_icons:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -215,14 +187,14 @@ packages:
 | 
				
			|||||||
      name: flutter_local_notifications
 | 
					      name: flutter_local_notifications
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.0.1"
 | 
					    version: "12.0.3"
 | 
				
			||||||
  flutter_local_notifications_linux:
 | 
					  flutter_local_notifications_linux:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_local_notifications_linux
 | 
					      name: flutter_local_notifications_linux
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.0"
 | 
					    version: "2.0.0"
 | 
				
			||||||
  flutter_local_notifications_platform_interface:
 | 
					  flutter_local_notifications_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -253,14 +225,14 @@ packages:
 | 
				
			|||||||
      name: fluttertoast
 | 
					      name: fluttertoast
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.0.9"
 | 
					    version: "8.1.1"
 | 
				
			||||||
  html:
 | 
					  html:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: html
 | 
					      name: html
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.15.0"
 | 
					    version: "0.15.1"
 | 
				
			||||||
  http:
 | 
					  http:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -274,14 +246,28 @@ packages:
 | 
				
			|||||||
      name: http_parser
 | 
					      name: http_parser
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.0.1"
 | 
					    version: "4.0.2"
 | 
				
			||||||
  image:
 | 
					  image:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image
 | 
					      name: image
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.2.0"
 | 
					    version: "3.2.2"
 | 
				
			||||||
 | 
					  install_plugin_v2:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: install_plugin_v2
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.0"
 | 
				
			||||||
 | 
					  installed_apps:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: installed_apps
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.3.1"
 | 
				
			||||||
  js:
 | 
					  js:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -302,7 +288,7 @@ packages:
 | 
				
			|||||||
      name: lints
 | 
					      name: lints
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.0"
 | 
					    version: "2.0.1"
 | 
				
			||||||
  matcher:
 | 
					  matcher:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -316,7 +302,7 @@ packages:
 | 
				
			|||||||
      name: material_color_utilities
 | 
					      name: material_color_utilities
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.2.0"
 | 
					    version: "0.1.5"
 | 
				
			||||||
  meta:
 | 
					  meta:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -338,6 +324,20 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.0"
 | 
					    version: "1.0.0"
 | 
				
			||||||
 | 
					  package_archive_info:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: package_archive_info
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.1.0"
 | 
				
			||||||
 | 
					  package_info:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: package_info
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.0.2"
 | 
				
			||||||
  path:
 | 
					  path:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -400,42 +400,42 @@ packages:
 | 
				
			|||||||
      name: permission_handler
 | 
					      name: permission_handler
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "10.0.2"
 | 
					    version: "10.2.0"
 | 
				
			||||||
  permission_handler_android:
 | 
					  permission_handler_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_android
 | 
					      name: permission_handler_android
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "10.1.0"
 | 
					    version: "10.2.0"
 | 
				
			||||||
  permission_handler_apple:
 | 
					  permission_handler_apple:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_apple
 | 
					      name: permission_handler_apple
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "9.0.4"
 | 
					    version: "9.0.7"
 | 
				
			||||||
  permission_handler_platform_interface:
 | 
					  permission_handler_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_platform_interface
 | 
					      name: permission_handler_platform_interface
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.8.0"
 | 
					    version: "3.9.0"
 | 
				
			||||||
  permission_handler_windows:
 | 
					  permission_handler_windows:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_windows
 | 
					      name: permission_handler_windows
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.1.0"
 | 
					    version: "0.1.2"
 | 
				
			||||||
  petitparser:
 | 
					  petitparser:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: petitparser
 | 
					      name: petitparser
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.0.0"
 | 
					    version: "5.1.0"
 | 
				
			||||||
  platform:
 | 
					  platform:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -463,49 +463,21 @@ packages:
 | 
				
			|||||||
      name: provider
 | 
					      name: provider
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.0.3"
 | 
					    version: "6.0.4"
 | 
				
			||||||
  share_plus:
 | 
					  share_plus:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: share_plus
 | 
					      name: share_plus
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.4.0"
 | 
					    version: "6.1.0"
 | 
				
			||||||
  share_plus_linux:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: share_plus_linux
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.0"
 | 
					 | 
				
			||||||
  share_plus_macos:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: share_plus_macos
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.1"
 | 
					 | 
				
			||||||
  share_plus_platform_interface:
 | 
					  share_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: share_plus_platform_interface
 | 
					      name: share_plus_platform_interface
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.3"
 | 
					    version: "3.2.0"
 | 
				
			||||||
  share_plus_web:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: share_plus_web
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.1"
 | 
					 | 
				
			||||||
  share_plus_windows:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: share_plus_windows
 | 
					 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.1"
 | 
					 | 
				
			||||||
  shared_preferences:
 | 
					  shared_preferences:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -519,7 +491,7 @@ packages:
 | 
				
			|||||||
      name: shared_preferences_android
 | 
					      name: shared_preferences_android
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.13"
 | 
					    version: "2.0.14"
 | 
				
			||||||
  shared_preferences_ios:
 | 
					  shared_preferences_ios:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -573,7 +545,7 @@ packages:
 | 
				
			|||||||
      name: source_span
 | 
					      name: source_span
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.9.1"
 | 
					    version: "1.9.0"
 | 
				
			||||||
  stack_trace:
 | 
					  stack_trace:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -587,7 +559,7 @@ packages:
 | 
				
			|||||||
      name: stream_channel
 | 
					      name: stream_channel
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.1"
 | 
					    version: "2.1.0"
 | 
				
			||||||
  string_scanner:
 | 
					  string_scanner:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -608,7 +580,7 @@ packages:
 | 
				
			|||||||
      name: test_api
 | 
					      name: test_api
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.4.14"
 | 
					    version: "0.4.12"
 | 
				
			||||||
  timezone:
 | 
					  timezone:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -636,7 +608,7 @@ packages:
 | 
				
			|||||||
      name: url_launcher_android
 | 
					      name: url_launcher_android
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.0.19"
 | 
					    version: "6.0.21"
 | 
				
			||||||
  url_launcher_ios:
 | 
					  url_launcher_ios:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -679,13 +651,20 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.1"
 | 
					    version: "3.0.1"
 | 
				
			||||||
 | 
					  uuid:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: uuid
 | 
				
			||||||
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.0.6"
 | 
				
			||||||
  vector_math:
 | 
					  vector_math:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: vector_math
 | 
					      name: vector_math
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.4"
 | 
					    version: "2.1.2"
 | 
				
			||||||
  webview_flutter:
 | 
					  webview_flutter:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -699,7 +678,7 @@ packages:
 | 
				
			|||||||
      name: webview_flutter_android
 | 
					      name: webview_flutter_android
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.10.3"
 | 
					    version: "2.10.4"
 | 
				
			||||||
  webview_flutter_platform_interface:
 | 
					  webview_flutter_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -720,14 +699,14 @@ packages:
 | 
				
			|||||||
      name: win32
 | 
					      name: win32
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.0"
 | 
					    version: "3.0.1"
 | 
				
			||||||
  workmanager:
 | 
					  workmanager:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: workmanager
 | 
					      name: workmanager
 | 
				
			||||||
      url: "https://pub.dartlang.org"
 | 
					      url: "https://pub.dartlang.org"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.5.0"
 | 
					    version: "0.5.1"
 | 
				
			||||||
  xdg_directories:
 | 
					  xdg_directories:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -750,5 +729,5 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.1"
 | 
					    version: "3.1.1"
 | 
				
			||||||
sdks:
 | 
					sdks:
 | 
				
			||||||
  dart: ">=2.19.0-79.0.dev <3.0.0"
 | 
					  dart: ">=2.18.2 <3.0.0"
 | 
				
			||||||
  flutter: ">=3.3.0"
 | 
					  flutter: ">=3.3.0"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						@@ -17,10 +17,10 @@ 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.5.4+25 # When changing this, update the tag in main() accordingly
 | 
					version: 0.6.2+46 # When changing this, update the tag in main() accordingly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: '>=2.19.0-79.0.dev <3.0.0'
 | 
					  sdk: '>=2.18.2 <3.0.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Dependencies specify other packages that your package needs in order to work.
 | 
					# Dependencies specify other packages that your package needs in order to work.
 | 
				
			||||||
# To automatically upgrade your package dependencies to the latest versions
 | 
					# To automatically upgrade your package dependencies to the latest versions
 | 
				
			||||||
@@ -38,7 +38,7 @@ dependencies:
 | 
				
			|||||||
  cupertino_icons: ^1.0.5
 | 
					  cupertino_icons: ^1.0.5
 | 
				
			||||||
  path_provider: ^2.0.11
 | 
					  path_provider: ^2.0.11
 | 
				
			||||||
  flutter_fgbg: ^0.2.0 # Try removing reliance on this
 | 
					  flutter_fgbg: ^0.2.0 # Try removing reliance on this
 | 
				
			||||||
  flutter_local_notifications: ^11.0.1
 | 
					  flutter_local_notifications: ^12.0.0
 | 
				
			||||||
  provider: ^6.0.3
 | 
					  provider: ^6.0.3
 | 
				
			||||||
  http: ^0.13.5
 | 
					  http: ^0.13.5
 | 
				
			||||||
  webview_flutter: ^3.0.4
 | 
					  webview_flutter: ^3.0.4
 | 
				
			||||||
@@ -49,11 +49,13 @@ dependencies:
 | 
				
			|||||||
  url_launcher: ^6.1.5
 | 
					  url_launcher: ^6.1.5
 | 
				
			||||||
  permission_handler: ^10.0.0
 | 
					  permission_handler: ^10.0.0
 | 
				
			||||||
  fluttertoast: ^8.0.9
 | 
					  fluttertoast: ^8.0.9
 | 
				
			||||||
  device_info_plus: ^4.1.2
 | 
					  device_info_plus: ^8.0.0
 | 
				
			||||||
  file_picker: ^5.1.0
 | 
					  file_picker: ^5.1.0
 | 
				
			||||||
  animations: ^2.0.4
 | 
					  animations: ^2.0.4
 | 
				
			||||||
  flutter_install_app: ^1.3.0
 | 
					  install_plugin_v2: ^1.0.0
 | 
				
			||||||
  share_plus: ^4.4.0
 | 
					  share_plus: ^6.0.1
 | 
				
			||||||
 | 
					  installed_apps: ^1.3.1
 | 
				
			||||||
 | 
					  package_archive_info: ^0.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
 
 | 
				
			|||||||