mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 06:43:29 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			main
			...
			experiment
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ab43856b90 | 
@@ -45,7 +45,7 @@ class APKCombo extends AppSource {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var html = parse(res.body);
 | 
			
		||||
    var html = parse(res.data);
 | 
			
		||||
    return html
 | 
			
		||||
        .querySelectorAll('#variants-tab > div > ul > li')
 | 
			
		||||
        .map((e) {
 | 
			
		||||
@@ -96,7 +96,7 @@ class APKCombo extends AppSource {
 | 
			
		||||
    if (preres.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(preres);
 | 
			
		||||
    }
 | 
			
		||||
    var res = parse(preres.body);
 | 
			
		||||
    var res = parse(preres.data);
 | 
			
		||||
    String? version = res.querySelector('div.version')?.text.trim();
 | 
			
		||||
    if (version == null) {
 | 
			
		||||
      throw NoVersionError();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -62,7 +62,7 @@ class APKMirror extends AppSource {
 | 
			
		||||
            : null;
 | 
			
		||||
    Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var items = parse(res.body).querySelectorAll('item');
 | 
			
		||||
      var items = parse(res.data).querySelectorAll('item');
 | 
			
		||||
      dynamic targetRelease;
 | 
			
		||||
      for (int i = 0; i < items.length; i++) {
 | 
			
		||||
        if (!fallbackToOlderReleases && i > 0) break;
 | 
			
		||||
 
 | 
			
		||||
@@ -61,8 +61,8 @@ class APKPure extends AppSource {
 | 
			
		||||
    var res = await sourceRequest('$standardUrl/download', additionalSettings);
 | 
			
		||||
    var resChangelog = await sourceRequest(standardUrl, additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200 && resChangelog.statusCode == 200) {
 | 
			
		||||
      var html = parse(res.body);
 | 
			
		||||
      var htmlChangelog = parse(resChangelog.body);
 | 
			
		||||
      var html = parse(res.data);
 | 
			
		||||
      var htmlChangelog = parse(resChangelog.data);
 | 
			
		||||
      String? version = html.querySelector('span.info-sdk span')?.text.trim();
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
 
 | 
			
		||||
@@ -38,10 +38,10 @@ class Aptoide extends AppSource {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
 | 
			
		||||
    var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.data);
 | 
			
		||||
    String? id;
 | 
			
		||||
    if (idMatch != null) {
 | 
			
		||||
      id = res.body.substring(idMatch.start + 12, idMatch.end);
 | 
			
		||||
      id = res.data.substring(idMatch.start + 12, idMatch.end);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
@@ -50,7 +50,7 @@ class Aptoide extends AppSource {
 | 
			
		||||
    if (res2.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
 | 
			
		||||
    return jsonDecode(res2.data)?['nodes']?['meta']?['data'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/gitlab.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
@@ -82,7 +82,7 @@ class FDroid extends AppSource {
 | 
			
		||||
        var res = await sourceRequest(
 | 
			
		||||
            'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
 | 
			
		||||
            additionalSettings);
 | 
			
		||||
        var lines = res.body.split('\n');
 | 
			
		||||
        var lines = res.data.split('\n');
 | 
			
		||||
        var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
 | 
			
		||||
        if (authorLines.isNotEmpty) {
 | 
			
		||||
          details.names.author =
 | 
			
		||||
@@ -112,7 +112,7 @@ class FDroid extends AppSource {
 | 
			
		||||
            details.changeLog = (await sourceRequest(
 | 
			
		||||
                    details.changeLog!.replaceFirst('/blob/', '/raw/'),
 | 
			
		||||
                    additionalSettings))
 | 
			
		||||
                .body;
 | 
			
		||||
                .data;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
@@ -132,7 +132,7 @@ class FDroid extends AppSource {
 | 
			
		||||
        'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      Map<String, List<String>> urlsWithDescriptions = {};
 | 
			
		||||
      parse(res.body).querySelectorAll('.package-header').forEach((e) {
 | 
			
		||||
      parse(res.data).querySelectorAll('.package-header').forEach((e) {
 | 
			
		||||
        String? url = e.attributes['href'];
 | 
			
		||||
        if (url != null) {
 | 
			
		||||
          try {
 | 
			
		||||
@@ -172,7 +172,7 @@ class FDroid extends AppSource {
 | 
			
		||||
            ? additionalSettings['apkFilterRegEx']
 | 
			
		||||
            : null;
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var response = jsonDecode(res.body);
 | 
			
		||||
      var response = jsonDecode(res.data);
 | 
			
		||||
      List<dynamic> releases = response['packages'] ?? [];
 | 
			
		||||
      if (apkFilterRegEx != null) {
 | 
			
		||||
        releases = releases.where((rel) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -61,9 +61,10 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    url = removeQueryParamsFromUrl(standardizeUrl(url));
 | 
			
		||||
    var res = await sourceRequestWithURLVariants(url, {});
 | 
			
		||||
    var ress = await sourceRequestWithURLVariants(url, {});
 | 
			
		||||
    var res = ress.value;
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var body = parse(res.body);
 | 
			
		||||
      var body = parse(res.data);
 | 
			
		||||
      Map<String, List<String>> results = {};
 | 
			
		||||
      body.querySelectorAll('application').toList().forEach((app) {
 | 
			
		||||
        String appId = app.attributes['id']!;
 | 
			
		||||
@@ -74,7 +75,7 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
            appName.contains(query) ||
 | 
			
		||||
            appDesc.contains(query)) {
 | 
			
		||||
          results[
 | 
			
		||||
              '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
 | 
			
		||||
              '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
 | 
			
		||||
            appName,
 | 
			
		||||
            appDesc
 | 
			
		||||
          ];
 | 
			
		||||
@@ -107,24 +108,24 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
    return app;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Response> sourceRequestWithURLVariants(
 | 
			
		||||
  Future<MapEntry<String, Response>> sourceRequestWithURLVariants(
 | 
			
		||||
    String url,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    var res = await sourceRequest(
 | 
			
		||||
        '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
 | 
			
		||||
        additionalSettings);
 | 
			
		||||
    var finalUrl = '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}';
 | 
			
		||||
    var res = await sourceRequest(finalUrl, additionalSettings);
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      var base = url.endsWith('/index.xml')
 | 
			
		||||
          ? url.split('/').reversed.toList().sublist(1).reversed.join('/')
 | 
			
		||||
          : url;
 | 
			
		||||
      res = await sourceRequest('$base/repo/index.xml', additionalSettings);
 | 
			
		||||
      finalUrl = '$base/repo/index.xml';
 | 
			
		||||
      res = await sourceRequest(finalUrl, additionalSettings);
 | 
			
		||||
      if (res.statusCode != 200) {
 | 
			
		||||
        res = await sourceRequest(
 | 
			
		||||
            '$base/fdroid/repo/index.xml', additionalSettings);
 | 
			
		||||
        finalUrl = '$base/fdroid/repo/index.xml';
 | 
			
		||||
        res = await sourceRequest(finalUrl, additionalSettings);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
    return MapEntry(finalUrl, res);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -142,10 +143,11 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
    if (appIdOrName == null) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    var res =
 | 
			
		||||
    var ress =
 | 
			
		||||
        await sourceRequestWithURLVariants(standardUrl, additionalSettings);
 | 
			
		||||
    var res = ress.value;
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var body = parse(res.body);
 | 
			
		||||
      var body = parse(res.data);
 | 
			
		||||
      var foundApps = body.querySelectorAll('application').where((element) {
 | 
			
		||||
        return element.attributes['id'] == appIdOrName;
 | 
			
		||||
      }).toList();
 | 
			
		||||
@@ -193,7 +195,7 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls = latestVersionReleases
 | 
			
		||||
          .map((e) =>
 | 
			
		||||
              '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
 | 
			
		||||
              '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
 | 
			
		||||
          .toList();
 | 
			
		||||
      return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
 | 
			
		||||
          AppNames(authorName, appName),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
@@ -112,12 +112,12 @@ class GitHub extends AppSource {
 | 
			
		||||
    ];
 | 
			
		||||
    for (var path in possibleBuildGradleLocations) {
 | 
			
		||||
      try {
 | 
			
		||||
        var res = await sourceRequest(
 | 
			
		||||
            '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
 | 
			
		||||
            additionalSettings);
 | 
			
		||||
        var finalUrl =
 | 
			
		||||
            '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path';
 | 
			
		||||
        var res = await sourceRequest(finalUrl, additionalSettings);
 | 
			
		||||
        if (res.statusCode == 200) {
 | 
			
		||||
          try {
 | 
			
		||||
            var body = jsonDecode(res.body);
 | 
			
		||||
            var body = jsonDecode(res.data);
 | 
			
		||||
            var trimmedLines = utf8
 | 
			
		||||
                .decode(base64
 | 
			
		||||
                    .decode(body['content'].toString().split('\n').join('')))
 | 
			
		||||
@@ -143,7 +143,7 @@ class GitHub extends AppSource {
 | 
			
		||||
            }
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            LogsProvider().add(
 | 
			
		||||
                'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}');
 | 
			
		||||
                'Error parsing build.gradle from $finalUrl: ${err.toString()}');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
@@ -256,11 +256,11 @@ class GitHub extends AppSource {
 | 
			
		||||
        }
 | 
			
		||||
        throw getObtainiumHttpError(res);
 | 
			
		||||
      }
 | 
			
		||||
      latestRelease = jsonDecode(res.body);
 | 
			
		||||
      latestRelease = jsonDecode(res.data);
 | 
			
		||||
    }
 | 
			
		||||
    Response res = await sourceRequest(requestUrl, additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var releases = jsonDecode(res.body) as List<dynamic>;
 | 
			
		||||
      var releases = jsonDecode(res.data) as List<dynamic>;
 | 
			
		||||
      if (latestRelease != null) {
 | 
			
		||||
        var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
 | 
			
		||||
        if (releases
 | 
			
		||||
@@ -466,7 +466,7 @@ class GitHub extends AppSource {
 | 
			
		||||
          ? int.parse(querySettings['minStarCount'])
 | 
			
		||||
          : 0;
 | 
			
		||||
      Map<String, List<String>> urlsWithDescriptions = {};
 | 
			
		||||
      for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
 | 
			
		||||
      for (var e in (jsonDecode(res.data)[rootProp] as List<dynamic>)) {
 | 
			
		||||
        if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) {
 | 
			
		||||
          urlsWithDescriptions.addAll({
 | 
			
		||||
            e['html_url'] as String: [
 | 
			
		||||
@@ -500,11 +500,13 @@ class GitHub extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rateLimitErrorCheck(Response res) {
 | 
			
		||||
    if (res.headers['x-ratelimit-remaining'] == '0') {
 | 
			
		||||
    String? rateLimitHeader;
 | 
			
		||||
    if (res.headers.map['x-ratelimit-remaining']?.isNotEmpty == true) {
 | 
			
		||||
      rateLimitHeader = res.headers.map['x-ratelimit-remaining']![0];
 | 
			
		||||
    }
 | 
			
		||||
    if (rateLimitHeader == '0') {
 | 
			
		||||
      throw RateLimitError(
 | 
			
		||||
          (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
 | 
			
		||||
                  60000000)
 | 
			
		||||
              .round());
 | 
			
		||||
          (int.parse(rateLimitHeader ?? '1800000000') / 60000000).round());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
@@ -81,7 +81,7 @@ class GitLab extends AppSource {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var json = jsonDecode(res.body) as List<dynamic>;
 | 
			
		||||
    var json = jsonDecode(res.data) as List<dynamic>;
 | 
			
		||||
    Map<String, List<String>> results = {};
 | 
			
		||||
    for (var element in json) {
 | 
			
		||||
      results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
 | 
			
		||||
@@ -131,7 +131,7 @@ class GitLab extends AppSource {
 | 
			
		||||
 | 
			
		||||
    // Extract .apk details from received data
 | 
			
		||||
    Iterable<APKDetails> apkDetailsList = [];
 | 
			
		||||
    var json = jsonDecode(res.body) as List<dynamic>;
 | 
			
		||||
    var json = jsonDecode(res.data) as List<dynamic>;
 | 
			
		||||
    apkDetailsList = json.map((e) {
 | 
			
		||||
      var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
 | 
			
		||||
          .map((e) {
 | 
			
		||||
@@ -152,9 +152,8 @@ class GitLab extends AppSource {
 | 
			
		||||
      var apkUrlsSet = apkUrlsFromAssets.toSet();
 | 
			
		||||
      apkUrlsSet.addAll(uploadedAPKsFromDescription);
 | 
			
		||||
      var releaseDateString = e['released_at'] ?? e['created_at'];
 | 
			
		||||
      DateTime? releaseDate = releaseDateString != null
 | 
			
		||||
          ? DateTime.parse(releaseDateString)
 | 
			
		||||
          : null;
 | 
			
		||||
      DateTime? releaseDate =
 | 
			
		||||
          releaseDateString != null ? DateTime.parse(releaseDateString) : null;
 | 
			
		||||
      return APKDetails(
 | 
			
		||||
          e['tag_name'] ?? e['name'],
 | 
			
		||||
          getApkUrlsFromUrls(apkUrlsSet.toList()),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
@@ -213,11 +213,11 @@ class HTML extends AppSource {
 | 
			
		||||
  // Given an HTTP response, grab some links according to the common additional settings
 | 
			
		||||
  // (those that apply to intermediate and final steps)
 | 
			
		||||
  Future<List<MapEntry<String, String>>> grabLinksCommon(
 | 
			
		||||
      Response res, Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
      Response res, Uri url, Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var html = parse(res.body);
 | 
			
		||||
    var html = parse(res.data);
 | 
			
		||||
    List<MapEntry<String, String>> allLinks = html
 | 
			
		||||
        .querySelectorAll('a')
 | 
			
		||||
        .map((element) => MapEntry(
 | 
			
		||||
@@ -226,13 +226,12 @@ class HTML extends AppSource {
 | 
			
		||||
                ? element.text
 | 
			
		||||
                : (element.attributes['href'] ?? '').split('/').last))
 | 
			
		||||
        .where((element) => element.key.isNotEmpty)
 | 
			
		||||
        .map((e) =>
 | 
			
		||||
            MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
 | 
			
		||||
        .map((e) => MapEntry(ensureAbsoluteUrl(e.key, url), e.value))
 | 
			
		||||
        .toList();
 | 
			
		||||
    if (allLinks.isEmpty) {
 | 
			
		||||
      allLinks = RegExp(
 | 
			
		||||
              r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
 | 
			
		||||
          .allMatches(res.body)
 | 
			
		||||
          .allMatches(res.data)
 | 
			
		||||
          .map((match) =>
 | 
			
		||||
              MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
 | 
			
		||||
          .toList();
 | 
			
		||||
@@ -285,6 +284,7 @@ class HTML extends AppSource {
 | 
			
		||||
    for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
 | 
			
		||||
      var intLinks = await grabLinksCommon(
 | 
			
		||||
          await sourceRequest(currentUrl, additionalSettings),
 | 
			
		||||
          Uri.parse(currentUrl),
 | 
			
		||||
          additionalSettings['intermediateLink'][i]);
 | 
			
		||||
      if (intLinks.isEmpty) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
@@ -298,8 +298,9 @@ class HTML extends AppSource {
 | 
			
		||||
    if (additionalSettings['directAPKLink'] != true) {
 | 
			
		||||
      Response res = await sourceRequest(currentUrl, additionalSettings);
 | 
			
		||||
      versionExtractionWholePageString =
 | 
			
		||||
          res.body.split('\r\n').join('\n').split('\n').join('\\n');
 | 
			
		||||
      links = await grabLinksCommon(res, additionalSettings);
 | 
			
		||||
          res.data.split('\r\n').join('\n').split('\n').join('\\n');
 | 
			
		||||
      links =
 | 
			
		||||
          await grabLinksCommon(res, Uri.parse(currentUrl), additionalSettings);
 | 
			
		||||
      links = filterApks(links, additionalSettings['apkFilterRegEx'],
 | 
			
		||||
          additionalSettings['invertAPKFilter']);
 | 
			
		||||
      if (links.isEmpty) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -57,8 +57,8 @@ class HuaweiAppGallery extends AppSource {
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const {}}) async {
 | 
			
		||||
    String dlUrl = getDlUrl(standardUrl);
 | 
			
		||||
    Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
 | 
			
		||||
    return res.headers['location'] != null
 | 
			
		||||
        ? appIdFromRedirectDlUrl(res.headers['location']!)
 | 
			
		||||
    return res.headers.map['location']?.isNotEmpty == true
 | 
			
		||||
        ? appIdFromRedirectDlUrl(res.headers.map['location']![0])
 | 
			
		||||
        : null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -72,9 +72,12 @@ class HuaweiAppGallery extends AppSource {
 | 
			
		||||
    if (res.headers['location'] == null) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    String appId = appIdFromRedirectDlUrl(res.headers['location']!);
 | 
			
		||||
    var relDateStr =
 | 
			
		||||
        res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
 | 
			
		||||
    String appId = appIdFromRedirectDlUrl(res.headers.map['location']![0]);
 | 
			
		||||
    var relDateStr = res.headers.map['location']?[0]
 | 
			
		||||
        .split('?')[0]
 | 
			
		||||
        .split('.')
 | 
			
		||||
        .reversed
 | 
			
		||||
        .toList()[1];
 | 
			
		||||
    var relDateStrAdj = relDateStr?.split('');
 | 
			
		||||
    var tempLen = relDateStrAdj?.length ?? 0;
 | 
			
		||||
    var i = 2;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +33,7 @@ class Jenkins extends AppSource {
 | 
			
		||||
    Response res = await sourceRequest(
 | 
			
		||||
        '$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var json = jsonDecode(res.body);
 | 
			
		||||
      var json = jsonDecode(res.data);
 | 
			
		||||
      var releaseDate = json['timestamp'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -33,7 +33,7 @@ class Mullvad extends AppSource {
 | 
			
		||||
    Response res = await sourceRequest(
 | 
			
		||||
        '$standardUrl/en/download/android', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var versions = parse(res.body)
 | 
			
		||||
      var versions = parse(res.data)
 | 
			
		||||
          .querySelectorAll('p')
 | 
			
		||||
          .map((e) => e.innerHtml)
 | 
			
		||||
          .where((p) => p.contains('Latest version: '))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +83,7 @@ class NeutronCode extends AppSource {
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await sourceRequest(standardUrl, additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var http = parse(res.body);
 | 
			
		||||
      var http = parse(res.data);
 | 
			
		||||
      var name = http.querySelector('.pd-title')?.innerHtml;
 | 
			
		||||
      var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
 | 
			
		||||
      if (filename == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +21,7 @@ class Signal extends AppSource {
 | 
			
		||||
    Response res = await sourceRequest(
 | 
			
		||||
        'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var json = jsonDecode(res.body);
 | 
			
		||||
      var json = jsonDecode(res.data);
 | 
			
		||||
      String? apkUrl = json['url'];
 | 
			
		||||
      List<String> apkUrls = apkUrl == null ? [] : [apkUrl];
 | 
			
		||||
      String? version = json['versionName'];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +49,7 @@ class SourceForge extends AppSource {
 | 
			
		||||
        '${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
 | 
			
		||||
        additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var parsedHtml = parse(res.body);
 | 
			
		||||
      var parsedHtml = parse(res.data);
 | 
			
		||||
      var allDownloadLinks = parsedHtml
 | 
			
		||||
          .querySelectorAll('guid')
 | 
			
		||||
          .map((e) => e.innerHtml)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/html.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -55,7 +55,7 @@ class SourceHut extends AppSource {
 | 
			
		||||
    Response res =
 | 
			
		||||
        await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var parsedHtml = parse(res.body);
 | 
			
		||||
      var parsedHtml = parse(res.data);
 | 
			
		||||
      List<APKDetails> apkDetailsList = [];
 | 
			
		||||
      int ind = 0;
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +85,7 @@ class SourceHut extends AppSource {
 | 
			
		||||
        var res2 = await sourceRequest(releasePage, additionalSettings);
 | 
			
		||||
        List<MapEntry<String, String>> apkUrls = [];
 | 
			
		||||
        if (res2.statusCode == 200) {
 | 
			
		||||
          apkUrls = getApkUrlsFromUrls(parse(res2.body)
 | 
			
		||||
          apkUrls = getApkUrlsFromUrls(parse(res2.data)
 | 
			
		||||
              .querySelectorAll('a')
 | 
			
		||||
              .map((e) => e.attributes['href'] ?? '')
 | 
			
		||||
              .where((e) => e.toLowerCase().endsWith('.apk'))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
@@ -38,7 +38,7 @@ class SteamMobile extends AppSource {
 | 
			
		||||
      }
 | 
			
		||||
      String apkInURLRegexPattern =
 | 
			
		||||
          '/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$';
 | 
			
		||||
      var links = parse(res.body)
 | 
			
		||||
      var links = parse(res.data)
 | 
			
		||||
          .querySelectorAll('a')
 | 
			
		||||
          .map((e) => e.attributes['href'] ?? '')
 | 
			
		||||
          .where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ class TelegramApp extends AppSource {
 | 
			
		||||
    Response res =
 | 
			
		||||
        await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var http = parse(res.body);
 | 
			
		||||
      var http = parse(res.data);
 | 
			
		||||
      var messages =
 | 
			
		||||
          http.querySelectorAll('.tgme_widget_message_text.js-message_text');
 | 
			
		||||
      var version = messages.isNotEmpty
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ class Uptodown extends AppSource {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var html = parse(res.body);
 | 
			
		||||
    var html = parse(res.data);
 | 
			
		||||
    String? version = html.querySelector('div.version')?.innerHtml;
 | 
			
		||||
    String? apkUrl =
 | 
			
		||||
        '${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download';
 | 
			
		||||
@@ -94,7 +94,7 @@ class Uptodown extends AppSource {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var html = parse(res.body);
 | 
			
		||||
    var html = parse(res.data);
 | 
			
		||||
    var finalUrlKey =
 | 
			
		||||
        html.querySelector('.post-download')?.attributes['data-url'];
 | 
			
		||||
    if (finalUrlKey == null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class VLC extends AppSource {
 | 
			
		||||
@@ -29,7 +30,7 @@ class VLC extends AppSource {
 | 
			
		||||
      String standardUrl, Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
    Response res = await sourceRequest(dwUrlBase, additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var dwLinks = parse(res.body)
 | 
			
		||||
      var dwLinks = parse(res.data)
 | 
			
		||||
          .querySelectorAll('a')
 | 
			
		||||
          .where((element) => element.attributes['href'] != 'last/')
 | 
			
		||||
          .map((e) => e.attributes['href']?.split('/')[0])
 | 
			
		||||
@@ -49,11 +50,11 @@ class VLC extends AppSource {
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    Response res = await get(
 | 
			
		||||
        Uri.parse('https://www.videolan.org/vlc/download-android.html'));
 | 
			
		||||
    Response res =
 | 
			
		||||
        await dio.get('https://www.videolan.org/vlc/download-android.html');
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var dwUrlBase = 'get.videolan.org/vlc-android';
 | 
			
		||||
      var dwLinks = parse(res.body)
 | 
			
		||||
      var dwLinks = parse(res.data)
 | 
			
		||||
          .querySelectorAll('a')
 | 
			
		||||
          .where((element) =>
 | 
			
		||||
              element.attributes['href']?.contains(dwUrlBase) ?? false)
 | 
			
		||||
@@ -84,14 +85,14 @@ class VLC extends AppSource {
 | 
			
		||||
    Response res = await sourceRequest(apkUrl, additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      String? apkUrl =
 | 
			
		||||
          parse(res.body).querySelector('#alt_link')?.attributes['href'];
 | 
			
		||||
          parse(res.data).querySelector('#alt_link')?.attributes['href'];
 | 
			
		||||
      if (apkUrl == null) {
 | 
			
		||||
        throw NoAPKError();
 | 
			
		||||
      }
 | 
			
		||||
      return apkUrl;
 | 
			
		||||
    } else if (res.statusCode == 500 &&
 | 
			
		||||
        res.body.toLowerCase().indexOf('mirror') > 0) {
 | 
			
		||||
      var html = parse(res.body);
 | 
			
		||||
        res.data.toLowerCase().indexOf('mirror') > 0) {
 | 
			
		||||
      var html = parse(res.data);
 | 
			
		||||
      var err = '';
 | 
			
		||||
      html.body?.nodes.forEach((element) {
 | 
			
		||||
        if (element.text != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +20,7 @@ class WhatsApp extends AppSource {
 | 
			
		||||
    Response res =
 | 
			
		||||
        await sourceRequest('$standardUrl/android', additionalSettings);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var targetLinks = parse(res.body)
 | 
			
		||||
      var targetLinks = parse(res.data)
 | 
			
		||||
          .querySelectorAll('a')
 | 
			
		||||
          .map((e) => e.attributes['href'] ?? '')
 | 
			
		||||
          .where((e) => e.isNotEmpty)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
 | 
			
		||||
List<MapEntry<Locale, String>> supportedLocales = const [
 | 
			
		||||
  MapEntry(Locale('en'), 'English'),
 | 
			
		||||
@@ -46,6 +47,9 @@ var fdroid = false;
 | 
			
		||||
 | 
			
		||||
final globalNavigatorKey = GlobalKey<NavigatorState>();
 | 
			
		||||
 | 
			
		||||
final dio = Dio(BaseOptions(
 | 
			
		||||
    responseType: ResponseType.plain, receiveDataWhenStatusError: true));
 | 
			
		||||
 | 
			
		||||
Future<void> loadTranslations() async {
 | 
			
		||||
  // See easy_localization/issues/210
 | 
			
		||||
  await EasyLocalizationController.initEasyLocation();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/github.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class GitHubStars implements MassAppUrlSource {
 | 
			
		||||
@@ -15,13 +16,12 @@ class GitHubStars implements MassAppUrlSource {
 | 
			
		||||
 | 
			
		||||
  Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
 | 
			
		||||
      String username, int page) async {
 | 
			
		||||
    Response res = await get(
 | 
			
		||||
        Uri.parse(
 | 
			
		||||
            'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
 | 
			
		||||
        headers: await GitHub().getRequestHeaders({}));
 | 
			
		||||
    Response res = await dio.get(
 | 
			
		||||
        'https://api.github.com/users/$username/starred?per_page=100&page=$page',
 | 
			
		||||
        options: Options(headers: await GitHub().getRequestHeaders({})));
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      Map<String, List<String>> urlsWithDescriptions = {};
 | 
			
		||||
      for (var e in (jsonDecode(res.body) as List<dynamic>)) {
 | 
			
		||||
      for (var e in (jsonDecode(res.data) as List<dynamic>)) {
 | 
			
		||||
        urlsWithDescriptions.addAll({
 | 
			
		||||
          e['html_url'] as String: [
 | 
			
		||||
            e['full_name'] as String,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,15 @@ import 'dart:async';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:crypto/crypto.dart';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
 | 
			
		||||
import 'package:android_intent_plus/flag.dart';
 | 
			
		||||
import 'package:android_package_installer/android_package_installer.dart';
 | 
			
		||||
import 'package:android_package_manager/android_package_manager.dart';
 | 
			
		||||
import 'package:connectivity_plus/connectivity_plus.dart';
 | 
			
		||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
@@ -28,7 +29,6 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:android_intent_plus/android_intent.dart';
 | 
			
		||||
import 'package:flutter_archive/flutter_archive.dart';
 | 
			
		||||
import 'package:shared_storage/shared_storage.dart' as saf;
 | 
			
		||||
@@ -146,10 +146,10 @@ Future<File> downloadFileWithRetry(
 | 
			
		||||
    Map<String, String>? headers,
 | 
			
		||||
    int retries = 3}) async {
 | 
			
		||||
  try {
 | 
			
		||||
    return await downloadFile(url, fileNameNoExt, onProgress, destDir,
 | 
			
		||||
    return await downloadApk(url, fileNameNoExt, onProgress, destDir,
 | 
			
		||||
        useExisting: useExisting, headers: headers);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if (retries > 0 && e is ClientException) {
 | 
			
		||||
    if (retries > 0 && e is DioException) {
 | 
			
		||||
      await Future.delayed(const Duration(seconds: 5));
 | 
			
		||||
      return await downloadFileWithRetry(
 | 
			
		||||
          url, fileNameNoExt, onProgress, destDir,
 | 
			
		||||
@@ -183,9 +183,162 @@ Future<String> checkPartialDownloadHashDynamic(String url,
 | 
			
		||||
  throw NoVersionError();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<File> downloadApk(
 | 
			
		||||
    String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
			
		||||
    {bool useExisting = true, Map<String, String>? headers}) async {
 | 
			
		||||
  var resHeaders = await getHeaders(url, headers: headers);
 | 
			
		||||
 | 
			
		||||
  String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk';
 | 
			
		||||
  if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
			
		||||
    ext = ext.substring(0, ext.length - 1);
 | 
			
		||||
  }
 | 
			
		||||
  if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
 | 
			
		||||
    ext = 'apk';
 | 
			
		||||
  }
 | 
			
		||||
  File file = File('$destDir/$fileNameNoExt.$ext');
 | 
			
		||||
 | 
			
		||||
  final contentLength = await getContentLengthIfRangeSupported(resHeaders);
 | 
			
		||||
 | 
			
		||||
  if (useExisting && file.existsSync()) {
 | 
			
		||||
    var length = file.lengthSync();
 | 
			
		||||
    if (contentLength == null) {
 | 
			
		||||
      return file;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (length == contentLength) {
 | 
			
		||||
        return file;
 | 
			
		||||
      }
 | 
			
		||||
      if (length > contentLength) {
 | 
			
		||||
        useExisting = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double progress = -1;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    if (contentLength == null) {
 | 
			
		||||
      Response response = await dio.download(
 | 
			
		||||
        url,
 | 
			
		||||
        file.path,
 | 
			
		||||
        options: Options(headers: headers),
 | 
			
		||||
        onReceiveProgress: (count, total) {
 | 
			
		||||
          progress = (total > 0 ? count / total * 100 : 30);
 | 
			
		||||
          if (onProgress != null) {
 | 
			
		||||
            onProgress(progress);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      if ((response.statusCode ?? 200) < 200 ||
 | 
			
		||||
          (response.statusCode ?? 200) > 299) {
 | 
			
		||||
        throw response.statusMessage ?? tr('unexpectedError');
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      var targetFileLength =
 | 
			
		||||
          useExisting && file.existsSync() ? file.lengthSync() : null;
 | 
			
		||||
      int bufferSize = 1024 * 1024; // 1 Megabyte
 | 
			
		||||
      final sink = file.openWrite(
 | 
			
		||||
        mode: useExisting ? FileMode.writeOnlyAppend : FileMode.writeOnly,
 | 
			
		||||
      );
 | 
			
		||||
      int rangeStart = targetFileLength ?? 0;
 | 
			
		||||
      int rangeEnd = min(
 | 
			
		||||
        rangeStart + bufferSize - 1,
 | 
			
		||||
        contentLength - 1,
 | 
			
		||||
      );
 | 
			
		||||
      if (onProgress != null) {
 | 
			
		||||
        progress = ((rangeStart / contentLength) * 100);
 | 
			
		||||
        onProgress(progress);
 | 
			
		||||
      }
 | 
			
		||||
      while (true) {
 | 
			
		||||
        var headersCurrent = headers ?? {};
 | 
			
		||||
        headersCurrent['range'] = 'bytes=$rangeStart-$rangeEnd';
 | 
			
		||||
        Response response = await dio.get(
 | 
			
		||||
          url,
 | 
			
		||||
          onReceiveProgress: (count, total) {
 | 
			
		||||
            if (onProgress != null) {
 | 
			
		||||
              final newProgress =
 | 
			
		||||
                  (((rangeStart + count) / contentLength) * 100);
 | 
			
		||||
              if (newProgress != progress) {
 | 
			
		||||
                progress = newProgress;
 | 
			
		||||
                onProgress(progress);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          options: Options(
 | 
			
		||||
            headers: headersCurrent,
 | 
			
		||||
            responseType: ResponseType.bytes,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if ((response.statusCode ?? 200) < 200 ||
 | 
			
		||||
            (response.statusCode ?? 200) > 299) {
 | 
			
		||||
          throw response.statusMessage ?? tr('unexpectedError');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final Uint8List data = response.data;
 | 
			
		||||
        sink.add(data);
 | 
			
		||||
        if (rangeEnd == contentLength - 1) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        rangeStart = rangeEnd + 1;
 | 
			
		||||
        rangeEnd = min(
 | 
			
		||||
          rangeStart + bufferSize - 1,
 | 
			
		||||
          contentLength - 1,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      await sink.flush();
 | 
			
		||||
      await sink.close();
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    if (onProgress != null) {
 | 
			
		||||
      onProgress(null);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return file;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<int?> getContentLengthIfRangeSupported(
 | 
			
		||||
    Map<String, String> headers) async {
 | 
			
		||||
  try {
 | 
			
		||||
    int? contentLength;
 | 
			
		||||
    {
 | 
			
		||||
      var contentLengthHeaderValue = headers['content-length'];
 | 
			
		||||
      if (contentLengthHeaderValue?.isNotEmpty == true) {
 | 
			
		||||
        contentLength = int.tryParse(contentLengthHeaderValue!);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    bool rangeFeatureEnabled = false;
 | 
			
		||||
    if (headers['accept-ranges']?.isNotEmpty == true) {
 | 
			
		||||
      rangeFeatureEnabled =
 | 
			
		||||
          headers['accept-ranges']!.trim().toLowerCase() == 'bytes';
 | 
			
		||||
    }
 | 
			
		||||
    if (!rangeFeatureEnabled) {
 | 
			
		||||
      contentLength = null;
 | 
			
		||||
    }
 | 
			
		||||
    return contentLength;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<Map<String, String>> getHeaders(String url,
 | 
			
		||||
    {Map<String, String>? headers}) async {
 | 
			
		||||
  var req = http.Request('GET', Uri.parse(url));
 | 
			
		||||
  if (headers != null) {
 | 
			
		||||
    req.headers.addAll(headers);
 | 
			
		||||
  }
 | 
			
		||||
  var client = http.Client();
 | 
			
		||||
  var response = await client.send(req);
 | 
			
		||||
  if (response.statusCode < 200 || response.statusCode > 299) {
 | 
			
		||||
    throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
 | 
			
		||||
  }
 | 
			
		||||
  var returnHeaders = response.headers;
 | 
			
		||||
  client.close();
 | 
			
		||||
  return returnHeaders;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
 | 
			
		||||
    {Map<String, String>? headers}) async {
 | 
			
		||||
  var req = Request('GET', Uri.parse(url));
 | 
			
		||||
  var req = http.Request('GET', Uri.parse(url));
 | 
			
		||||
  if (headers != null) {
 | 
			
		||||
    req.headers.addAll(headers);
 | 
			
		||||
  }
 | 
			
		||||
@@ -199,58 +352,41 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
 | 
			
		||||
  return hashListOfLists(bytes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<File> downloadFile(
 | 
			
		||||
    String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
			
		||||
    {bool useExisting = true, Map<String, String>? headers}) async {
 | 
			
		||||
  var req = Request('GET', Uri.parse(url));
 | 
			
		||||
  if (headers != null) {
 | 
			
		||||
    req.headers.addAll(headers);
 | 
			
		||||
  }
 | 
			
		||||
  var client = http.Client();
 | 
			
		||||
  StreamedResponse response = await client.send(req);
 | 
			
		||||
  String ext =
 | 
			
		||||
      response.headers['content-disposition']?.split('.').last ?? 'apk';
 | 
			
		||||
  if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
			
		||||
    ext = ext.substring(0, ext.length - 1);
 | 
			
		||||
  }
 | 
			
		||||
  if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
 | 
			
		||||
    ext = 'apk';
 | 
			
		||||
  }
 | 
			
		||||
  File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
 | 
			
		||||
  if (!(downloadedFile.existsSync() && useExisting)) {
 | 
			
		||||
    File tempDownloadedFile = File('${downloadedFile.path}.part');
 | 
			
		||||
    if (tempDownloadedFile.existsSync()) {
 | 
			
		||||
      tempDownloadedFile.deleteSync(recursive: true);
 | 
			
		||||
    }
 | 
			
		||||
    var length = response.contentLength;
 | 
			
		||||
    var received = 0;
 | 
			
		||||
    double? progress;
 | 
			
		||||
    var sink = tempDownloadedFile.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) {
 | 
			
		||||
      tempDownloadedFile.deleteSync(recursive: true);
 | 
			
		||||
      throw response.reasonPhrase ?? tr('unexpectedError');
 | 
			
		||||
    }
 | 
			
		||||
    if (tempDownloadedFile.existsSync()) {
 | 
			
		||||
      tempDownloadedFile.renameSync(downloadedFile.path);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    client.close();
 | 
			
		||||
  }
 | 
			
		||||
  return downloadedFile;
 | 
			
		||||
}
 | 
			
		||||
// Future<File> downloadFile(
 | 
			
		||||
//     String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
			
		||||
//     {bool useExisting = true, Map<String, String>? headers}) async {
 | 
			
		||||
//   var resHead = await dio.head(url);
 | 
			
		||||
//   String ext =
 | 
			
		||||
//       resHead.headers.map['content-disposition']?[0].split('.').last ?? 'apk';
 | 
			
		||||
//   if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
			
		||||
//     ext = ext.substring(0, ext.length - 1);
 | 
			
		||||
//   }
 | 
			
		||||
//   if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
 | 
			
		||||
//     ext = 'apk';
 | 
			
		||||
//   }
 | 
			
		||||
//   File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
 | 
			
		||||
//   if (!(downloadedFile.existsSync() && useExisting)) {
 | 
			
		||||
//     double? progress;
 | 
			
		||||
//     var response = await dio.download(
 | 
			
		||||
//       url,
 | 
			
		||||
//       downloadedFile.path,
 | 
			
		||||
//       options: Options(headers: headers),
 | 
			
		||||
//       onReceiveProgress: (count, total) {
 | 
			
		||||
//         progress = (total > 0 ? count / total * 100 : 30);
 | 
			
		||||
//         if (onProgress != null) {
 | 
			
		||||
//           onProgress(progress);
 | 
			
		||||
//         }
 | 
			
		||||
//       },
 | 
			
		||||
//     );
 | 
			
		||||
//     if (onProgress != null) {
 | 
			
		||||
//       onProgress(null);
 | 
			
		||||
//     }
 | 
			
		||||
//     if (response.statusCode != 200) {
 | 
			
		||||
//       throw response.statusMessage ?? tr('unexpectedError');
 | 
			
		||||
//     }
 | 
			
		||||
//   }
 | 
			
		||||
//   return downloadedFile;
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
Future<PackageInfo?> getInstalledInfo(String? packageName,
 | 
			
		||||
    {bool printErr = true}) async {
 | 
			
		||||
@@ -1621,7 +1757,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
            // Next task interval is based on the error with the longest retry time
 | 
			
		||||
            int minRetryIntervalForThisApp = err is RateLimitError
 | 
			
		||||
                ? (err.remainingMinutes * 60)
 | 
			
		||||
                : e is ClientException
 | 
			
		||||
                : e is DioException
 | 
			
		||||
                    ? (15 * 60)
 | 
			
		||||
                    : (toCheckApp.value + 1);
 | 
			
		||||
            if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/dom.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/apkmirror.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/apkpure.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/aptoide.dart';
 | 
			
		||||
@@ -31,6 +31,7 @@ import 'package:obtainium/app_sources/vlc.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/whatsapp.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -442,16 +443,9 @@ abstract class AppSource {
 | 
			
		||||
      String url, Map<String, dynamic> additionalSettings,
 | 
			
		||||
      {bool followRedirects = true}) async {
 | 
			
		||||
    var requestHeaders = await getRequestHeaders(additionalSettings);
 | 
			
		||||
    if (requestHeaders != null || followRedirects == false) {
 | 
			
		||||
      var req = Request('GET', Uri.parse(url));
 | 
			
		||||
      req.followRedirects = followRedirects;
 | 
			
		||||
      if (requestHeaders != null) {
 | 
			
		||||
        req.headers.addAll(requestHeaders);
 | 
			
		||||
      }
 | 
			
		||||
      return Response.fromStream(await Client().send(req));
 | 
			
		||||
    } else {
 | 
			
		||||
      return get(Uri.parse(url));
 | 
			
		||||
    }
 | 
			
		||||
    return await dio.get(url,
 | 
			
		||||
        options:
 | 
			
		||||
            Options(headers: requestHeaders, followRedirects: followRedirects));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
@@ -618,10 +612,10 @@ abstract class AppSource {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ObtainiumError getObtainiumHttpError(Response res) {
 | 
			
		||||
  return ObtainiumError((res.reasonPhrase != null &&
 | 
			
		||||
          res.reasonPhrase != null &&
 | 
			
		||||
          res.reasonPhrase!.isNotEmpty)
 | 
			
		||||
      ? res.reasonPhrase!
 | 
			
		||||
  return ObtainiumError((res.statusMessage != null &&
 | 
			
		||||
          res.statusMessage != null &&
 | 
			
		||||
          res.statusMessage!.isNotEmpty)
 | 
			
		||||
      ? res.statusMessage!
 | 
			
		||||
      : tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -202,6 +202,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.0.0"
 | 
			
		||||
  dio:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: dio
 | 
			
		||||
      sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.4.1"
 | 
			
		||||
  dynamic_color:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,7 @@ dependencies:
 | 
			
		||||
  crypto: ^3.0.3
 | 
			
		||||
  app_links: ^3.5.0
 | 
			
		||||
  background_fetch: ^1.2.1
 | 
			
		||||
  dio: ^5.4.1
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user