mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Merge pull request #1478 from ImranR98/dev
Resume failed downloads when possible instead of starting again (#634), Typo: 'Installed' not 'Updated' (#1469) + Bugfix: Don't incorrectly show message when user cancel, Add changelog button to app page (#1474)
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -42,7 +42,6 @@ jobs:
 | 
			
		||||
          if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi
 | 
			
		||||
          echo "beta=$BETA" >> $GITHUB_OUTPUT
 | 
			
		||||
          TAG="v$VERSION"
 | 
			
		||||
          if [ $BETA == true ]; then TAG="$TAG"-beta; fi
 | 
			
		||||
          echo "tag=$TAG" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
      - name: Build APKs
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,9 @@
 | 
			
		||||
plugins {
 | 
			
		||||
    id "com.android.application"
 | 
			
		||||
    id "kotlin-android"
 | 
			
		||||
    id "dev.flutter.flutter-gradle-plugin"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def localProperties = new Properties()
 | 
			
		||||
def localPropertiesFile = rootProject.file('local.properties')
 | 
			
		||||
if (localPropertiesFile.exists()) {
 | 
			
		||||
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
 | 
			
		||||
if (flutterRoot == null) {
 | 
			
		||||
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
 | 
			
		||||
if (flutterVersionCode == null) {
 | 
			
		||||
    flutterVersionCode = '1'
 | 
			
		||||
@@ -21,11 +22,6 @@ if (flutterVersionName == null) {
 | 
			
		||||
    flutterVersionName = '1.0'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'dev.rikka.tools.refine'
 | 
			
		||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 | 
			
		||||
 | 
			
		||||
def keystoreProperties = new Properties()
 | 
			
		||||
def keystorePropertiesFile = rootProject.file('key.properties')
 | 
			
		||||
if (keystorePropertiesFile.exists()) {
 | 
			
		||||
@@ -33,7 +29,8 @@ if (keystorePropertiesFile.exists()) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion rootProject.ext.compileSdkVersion
 | 
			
		||||
    namespace "dev.imranr.obtainium"
 | 
			
		||||
    compileSdk flutter.compileSdkVersion
 | 
			
		||||
    ndkVersion flutter.ndkVersion
 | 
			
		||||
 | 
			
		||||
    compileOptions {
 | 
			
		||||
@@ -54,7 +51,7 @@ android {
 | 
			
		||||
        // You can update the following values to match your application needs.
 | 
			
		||||
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
 | 
			
		||||
        minSdkVersion 24
 | 
			
		||||
        targetSdkVersion rootProject.ext.targetSdkVersion
 | 
			
		||||
        targetSdkVersion flutter.targetSdkVersion
 | 
			
		||||
        versionCode flutterVersionCode.toInteger()
 | 
			
		||||
        versionName flutterVersionName
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,3 @@
 | 
			
		||||
buildscript {
 | 
			
		||||
    ext.kotlin_version = '1.8.10'
 | 
			
		||||
    ext {
 | 
			
		||||
        compileSdkVersion   = 34                // or latest
 | 
			
		||||
        targetSdkVersion    = 34                // or latest
 | 
			
		||||
        appCompatVersion    = "1.4.2"           // or latest
 | 
			
		||||
    }
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath "com.android.tools.build:gradle:7.4.2"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "dev.rikka.tools.refine:gradle-plugin:4.3.1"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
allprojects {
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,25 @@
 | 
			
		||||
include ':app'
 | 
			
		||||
pluginManagement {
 | 
			
		||||
    def flutterSdkPath = {
 | 
			
		||||
        def properties = new Properties()
 | 
			
		||||
        file("local.properties").withInputStream { properties.load(it) }
 | 
			
		||||
        def flutterSdkPath = properties.getProperty("flutter.sdk")
 | 
			
		||||
        assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
 | 
			
		||||
        return flutterSdkPath
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
 | 
			
		||||
def properties = new Properties()
 | 
			
		||||
    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
 | 
			
		||||
 | 
			
		||||
assert localPropertiesFile.exists()
 | 
			
		||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
        gradlePluginPortal()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
 | 
			
		||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
 | 
			
		||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
 | 
			
		||||
plugins {
 | 
			
		||||
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
 | 
			
		||||
    id "com.android.application" version "7.4.2" apply false
 | 
			
		||||
    id "org.jetbrains.kotlin.android" version "1.8.10" apply false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
include ":app"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/pages/apps.dart';
 | 
			
		||||
import 'package:obtainium/pages/settings.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
@@ -108,6 +109,7 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
        infoLines =
 | 
			
		||||
            '$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}';
 | 
			
		||||
      }
 | 
			
		||||
      var changeLogFn = app != null ? getChangeLogFn(context, app.app) : null;
 | 
			
		||||
      return Column(
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
@@ -125,13 +127,26 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                        .textTheme
 | 
			
		||||
                        .bodyLarge!
 | 
			
		||||
                        .copyWith(fontWeight: FontWeight.bold)),
 | 
			
		||||
                app?.app.releaseDate == null
 | 
			
		||||
                    ? const SizedBox.shrink()
 | 
			
		||||
                    : Text(
 | 
			
		||||
                        app!.app.releaseDate.toString(),
 | 
			
		||||
                        textAlign: TextAlign.center,
 | 
			
		||||
                        style: Theme.of(context).textTheme.labelSmall,
 | 
			
		||||
                      ),
 | 
			
		||||
                changeLogFn != null || app?.app.releaseDate != null
 | 
			
		||||
                    ? GestureDetector(
 | 
			
		||||
                        onTap: changeLogFn,
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          app?.app.releaseDate == null
 | 
			
		||||
                              ? tr('changes')
 | 
			
		||||
                              : app!.app.releaseDate.toString(),
 | 
			
		||||
                          textAlign: TextAlign.center,
 | 
			
		||||
                          style:
 | 
			
		||||
                              Theme.of(context).textTheme.labelSmall!.copyWith(
 | 
			
		||||
                                    decoration: changeLogFn != null
 | 
			
		||||
                                        ? TextDecoration.underline
 | 
			
		||||
                                        : null,
 | 
			
		||||
                                    fontStyle: changeLogFn != null
 | 
			
		||||
                                        ? FontStyle.italic
 | 
			
		||||
                                        : null,
 | 
			
		||||
                                  ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                const SizedBox(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -361,6 +376,9 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                !areDownloadsRunning
 | 
			
		||||
            ? () async {
 | 
			
		||||
                try {
 | 
			
		||||
                  var successMessage = app?.app.installedVersion == null
 | 
			
		||||
                      ? tr('installed')
 | 
			
		||||
                      : tr('appsUpdated');
 | 
			
		||||
                  HapticFeedback.heavyImpact();
 | 
			
		||||
                  var res = await appsProvider.downloadAndInstallLatestApps(
 | 
			
		||||
                    app?.app.id != null ? [app!.app.id] : [],
 | 
			
		||||
@@ -368,7 +386,7 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                  );
 | 
			
		||||
                  if (res.isNotEmpty && !trackOnly) {
 | 
			
		||||
                    // ignore: use_build_context_synchronously
 | 
			
		||||
                    showMessage(tr('appsUpdated'), context);
 | 
			
		||||
                    showMessage(successMessage, context);
 | 
			
		||||
                  }
 | 
			
		||||
                  if (res.isNotEmpty && mounted) {
 | 
			
		||||
                    Navigator.of(context).pop();
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,92 @@ class AppsPage extends StatefulWidget {
 | 
			
		||||
  State<AppsPage> createState() => AppsPageState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
showChangeLogDialog(BuildContext context, App app, String? changesUrl,
 | 
			
		||||
    AppSource appSource, String changeLog) {
 | 
			
		||||
  showDialog(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (BuildContext context) {
 | 
			
		||||
        return GeneratedFormModal(
 | 
			
		||||
          title: tr('changes'),
 | 
			
		||||
          items: const [],
 | 
			
		||||
          message: app.latestVersion,
 | 
			
		||||
          additionalWidgets: [
 | 
			
		||||
            changesUrl != null
 | 
			
		||||
                ? GestureDetector(
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      changesUrl,
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                          decoration: TextDecoration.underline,
 | 
			
		||||
                          fontStyle: FontStyle.italic),
 | 
			
		||||
                    ),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      launchUrlString(changesUrl,
 | 
			
		||||
                          mode: LaunchMode.externalApplication);
 | 
			
		||||
                    },
 | 
			
		||||
                  )
 | 
			
		||||
                : const SizedBox.shrink(),
 | 
			
		||||
            changesUrl != null
 | 
			
		||||
                ? const SizedBox(
 | 
			
		||||
                    height: 16,
 | 
			
		||||
                  )
 | 
			
		||||
                : const SizedBox.shrink(),
 | 
			
		||||
            appSource.changeLogIfAnyIsMarkDown
 | 
			
		||||
                ? SizedBox(
 | 
			
		||||
                    width: MediaQuery.of(context).size.width,
 | 
			
		||||
                    height: MediaQuery.of(context).size.height - 350,
 | 
			
		||||
                    child: Markdown(
 | 
			
		||||
                      data: changeLog,
 | 
			
		||||
                      onTapLink: (text, href, title) {
 | 
			
		||||
                        if (href != null) {
 | 
			
		||||
                          launchUrlString(
 | 
			
		||||
                              href.startsWith('http://') ||
 | 
			
		||||
                                      href.startsWith('https://')
 | 
			
		||||
                                  ? href
 | 
			
		||||
                                  : '${Uri.parse(app.url).origin}/$href',
 | 
			
		||||
                              mode: LaunchMode.externalApplication);
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
                      extensionSet: md.ExtensionSet(
 | 
			
		||||
                        md.ExtensionSet.gitHubFlavored.blockSyntaxes,
 | 
			
		||||
                        [
 | 
			
		||||
                          md.EmojiSyntax(),
 | 
			
		||||
                          ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ))
 | 
			
		||||
                : Text(changeLog),
 | 
			
		||||
          ],
 | 
			
		||||
          singleNullReturnButton: tr('ok'),
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
getChangeLogFn(BuildContext context, App app) {
 | 
			
		||||
  AppSource appSource =
 | 
			
		||||
      SourceProvider().getSource(app.url, overrideSource: app.overrideSource);
 | 
			
		||||
  String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url);
 | 
			
		||||
  String? changeLog = app.changeLog;
 | 
			
		||||
  if (changeLog?.split('\n').length == 1) {
 | 
			
		||||
    if (RegExp(
 | 
			
		||||
            '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
 | 
			
		||||
        .hasMatch(changeLog!)) {
 | 
			
		||||
      if (changesUrl == null) {
 | 
			
		||||
        changesUrl = changeLog;
 | 
			
		||||
        changeLog = null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return (changeLog == null && changesUrl == null)
 | 
			
		||||
      ? null
 | 
			
		||||
      : () {
 | 
			
		||||
          if (changeLog != null) {
 | 
			
		||||
            showChangeLogDialog(context, app, changesUrl, appSource, changeLog);
 | 
			
		||||
          } else {
 | 
			
		||||
            launchUrlString(changesUrl!, mode: LaunchMode.externalApplication);
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class AppsPageState extends State<AppsPage> {
 | 
			
		||||
  AppsFilter filter = AppsFilter();
 | 
			
		||||
  final AppsFilter neutralFilter = AppsFilter();
 | 
			
		||||
@@ -262,66 +348,6 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
        .where((a) => selectedAppIds.contains(a.id))
 | 
			
		||||
        .toSet();
 | 
			
		||||
 | 
			
		||||
    showChangeLogDialog(
 | 
			
		||||
        String? changesUrl, AppSource appSource, String changeLog, int index) {
 | 
			
		||||
      showDialog(
 | 
			
		||||
          context: context,
 | 
			
		||||
          builder: (BuildContext context) {
 | 
			
		||||
            return GeneratedFormModal(
 | 
			
		||||
              title: tr('changes'),
 | 
			
		||||
              items: const [],
 | 
			
		||||
              message: listedApps[index].app.latestVersion,
 | 
			
		||||
              additionalWidgets: [
 | 
			
		||||
                changesUrl != null
 | 
			
		||||
                    ? GestureDetector(
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          changesUrl,
 | 
			
		||||
                          style: const TextStyle(
 | 
			
		||||
                              decoration: TextDecoration.underline,
 | 
			
		||||
                              fontStyle: FontStyle.italic),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          launchUrlString(changesUrl,
 | 
			
		||||
                              mode: LaunchMode.externalApplication);
 | 
			
		||||
                        },
 | 
			
		||||
                      )
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                changesUrl != null
 | 
			
		||||
                    ? const SizedBox(
 | 
			
		||||
                        height: 16,
 | 
			
		||||
                      )
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                appSource.changeLogIfAnyIsMarkDown
 | 
			
		||||
                    ? SizedBox(
 | 
			
		||||
                        width: MediaQuery.of(context).size.width,
 | 
			
		||||
                        height: MediaQuery.of(context).size.height - 350,
 | 
			
		||||
                        child: Markdown(
 | 
			
		||||
                          data: changeLog,
 | 
			
		||||
                          onTapLink: (text, href, title) {
 | 
			
		||||
                            if (href != null) {
 | 
			
		||||
                              launchUrlString(
 | 
			
		||||
                                  href.startsWith('http://') ||
 | 
			
		||||
                                          href.startsWith('https://')
 | 
			
		||||
                                      ? href
 | 
			
		||||
                                      : '${Uri.parse(listedApps[index].app.url).origin}/$href',
 | 
			
		||||
                                  mode: LaunchMode.externalApplication);
 | 
			
		||||
                            }
 | 
			
		||||
                          },
 | 
			
		||||
                          extensionSet: md.ExtensionSet(
 | 
			
		||||
                            md.ExtensionSet.gitHubFlavored.blockSyntaxes,
 | 
			
		||||
                            [
 | 
			
		||||
                              md.EmojiSyntax(),
 | 
			
		||||
                              ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                        ))
 | 
			
		||||
                    : Text(changeLog),
 | 
			
		||||
              ],
 | 
			
		||||
              singleNullReturnButton: tr('ok'),
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getLoadingWidgets() {
 | 
			
		||||
      return [
 | 
			
		||||
        if (listedApps.isEmpty)
 | 
			
		||||
@@ -351,35 +377,6 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChangeLogFn(int appIndex) {
 | 
			
		||||
      AppSource appSource = SourceProvider().getSource(
 | 
			
		||||
          listedApps[appIndex].app.url,
 | 
			
		||||
          overrideSource: listedApps[appIndex].app.overrideSource);
 | 
			
		||||
      String? changesUrl =
 | 
			
		||||
          appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
 | 
			
		||||
      String? changeLog = listedApps[appIndex].app.changeLog;
 | 
			
		||||
      if (changeLog?.split('\n').length == 1) {
 | 
			
		||||
        if (RegExp(
 | 
			
		||||
                '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
 | 
			
		||||
            .hasMatch(changeLog!)) {
 | 
			
		||||
          if (changesUrl == null) {
 | 
			
		||||
            changesUrl = changeLog;
 | 
			
		||||
            changeLog = null;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return (changeLog == null && changesUrl == null)
 | 
			
		||||
          ? null
 | 
			
		||||
          : () {
 | 
			
		||||
              if (changeLog != null) {
 | 
			
		||||
                showChangeLogDialog(changesUrl, appSource, changeLog, appIndex);
 | 
			
		||||
              } else {
 | 
			
		||||
                launchUrlString(changesUrl!,
 | 
			
		||||
                    mode: LaunchMode.externalApplication);
 | 
			
		||||
              }
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getUpdateButton(int appIndex) {
 | 
			
		||||
      return IconButton(
 | 
			
		||||
          visualDensity: VisualDensity.compact,
 | 
			
		||||
@@ -444,7 +441,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSingleAppHorizTile(int index) {
 | 
			
		||||
      var showChangesFn = getChangeLogFn(index);
 | 
			
		||||
      var showChangesFn = getChangeLogFn(context, listedApps[index].app);
 | 
			
		||||
      var hasUpdate = listedApps[index].app.installedVersion != null &&
 | 
			
		||||
          listedApps[index].app.installedVersion !=
 | 
			
		||||
              listedApps[index].app.latestVersion;
 | 
			
		||||
 
 | 
			
		||||
@@ -202,14 +202,18 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
 | 
			
		||||
Future<File> downloadFile(
 | 
			
		||||
    String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
			
		||||
    {bool useExisting = true, Map<String, String>? headers}) async {
 | 
			
		||||
  // Send the initial request but cancel it as soon as you have the headers
 | 
			
		||||
  var reqHeaders = headers ?? {};
 | 
			
		||||
  var req = Request('GET', Uri.parse(url));
 | 
			
		||||
  if (headers != null) {
 | 
			
		||||
    req.headers.addAll(headers);
 | 
			
		||||
  }
 | 
			
		||||
  req.headers.addAll(reqHeaders);
 | 
			
		||||
  var client = http.Client();
 | 
			
		||||
  StreamedResponse response = await client.send(req);
 | 
			
		||||
  String ext =
 | 
			
		||||
      response.headers['content-disposition']?.split('.').last ?? 'apk';
 | 
			
		||||
  var resHeaders = response.headers;
 | 
			
		||||
 | 
			
		||||
  // Use the headers to decide what the file extension is, and
 | 
			
		||||
  // whether it supports partial downloads (range request), and
 | 
			
		||||
  // what the total size of the file is (if provided)
 | 
			
		||||
  String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk';
 | 
			
		||||
  if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
			
		||||
    ext = ext.substring(0, ext.length - 1);
 | 
			
		||||
  }
 | 
			
		||||
@@ -217,41 +221,107 @@ Future<File> downloadFile(
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
  bool rangeFeatureEnabled = false;
 | 
			
		||||
  if (resHeaders['accept-ranges']?.isNotEmpty == true) {
 | 
			
		||||
    rangeFeatureEnabled =
 | 
			
		||||
        resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If you have an existing file that is usable,
 | 
			
		||||
  // decide whether you can use it (either return full or resume partial)
 | 
			
		||||
  var fullContentLength = response.contentLength;
 | 
			
		||||
  if (useExisting && downloadedFile.existsSync()) {
 | 
			
		||||
    var length = downloadedFile.lengthSync();
 | 
			
		||||
    if (fullContentLength == null) {
 | 
			
		||||
      // Assume full
 | 
			
		||||
      client.close();
 | 
			
		||||
      return downloadedFile;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Check if resume needed/possible
 | 
			
		||||
      if (length == fullContentLength) {
 | 
			
		||||
        client.close();
 | 
			
		||||
        return downloadedFile;
 | 
			
		||||
      }
 | 
			
		||||
      return s;
 | 
			
		||||
    }).pipe(sink);
 | 
			
		||||
    await sink.close();
 | 
			
		||||
    progress = null;
 | 
			
		||||
      if (length > fullContentLength) {
 | 
			
		||||
        useExisting = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Download to a '.temp' file (to distinguish btn. complete/incomplete files)
 | 
			
		||||
  File tempDownloadedFile = File('${downloadedFile.path}.part');
 | 
			
		||||
 | 
			
		||||
  // If the range feature is not available (or you need to start a ranged req from 0),
 | 
			
		||||
  // complete the already-started request, else cancel it and start a ranged request,
 | 
			
		||||
  // and open the file for writing in the appropriate mode
 | 
			
		||||
  var targetFileLength = useExisting && tempDownloadedFile.existsSync()
 | 
			
		||||
      ? tempDownloadedFile.lengthSync()
 | 
			
		||||
      : null;
 | 
			
		||||
  int rangeStart = targetFileLength ?? 0;
 | 
			
		||||
  IOSink? sink;
 | 
			
		||||
  if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) {
 | 
			
		||||
    client.close();
 | 
			
		||||
    client = http.Client();
 | 
			
		||||
    req = Request('GET', Uri.parse(url));
 | 
			
		||||
    req.headers.addAll(reqHeaders);
 | 
			
		||||
    req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'});
 | 
			
		||||
    response = await client.send(req);
 | 
			
		||||
    sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend);
 | 
			
		||||
  } else if (tempDownloadedFile.existsSync()) {
 | 
			
		||||
    tempDownloadedFile.deleteSync(recursive: true);
 | 
			
		||||
  }
 | 
			
		||||
  sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly);
 | 
			
		||||
 | 
			
		||||
  // Perform the download
 | 
			
		||||
  var received = 0;
 | 
			
		||||
  double? progress;
 | 
			
		||||
  if (rangeStart > 0 && fullContentLength != null) {
 | 
			
		||||
    received = rangeStart;
 | 
			
		||||
  }
 | 
			
		||||
  await response.stream.map((s) {
 | 
			
		||||
    received += s.length;
 | 
			
		||||
    progress =
 | 
			
		||||
        (fullContentLength != null ? (received / fullContentLength) * 100 : 30);
 | 
			
		||||
    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 s;
 | 
			
		||||
  }).pipe(sink);
 | 
			
		||||
  await sink.close();
 | 
			
		||||
  progress = null;
 | 
			
		||||
  if (onProgress != null) {
 | 
			
		||||
    onProgress(progress);
 | 
			
		||||
  }
 | 
			
		||||
  if (response.statusCode < 200 || response.statusCode > 299) {
 | 
			
		||||
    tempDownloadedFile.deleteSync(recursive: true);
 | 
			
		||||
    throw response.reasonPhrase ?? tr('unexpectedError');
 | 
			
		||||
  }
 | 
			
		||||
  print(tempDownloadedFile.lengthSync());
 | 
			
		||||
  print(fullContentLength);
 | 
			
		||||
  if (tempDownloadedFile.existsSync()) {
 | 
			
		||||
    tempDownloadedFile.renameSync(downloadedFile.path);
 | 
			
		||||
  }
 | 
			
		||||
  client.close();
 | 
			
		||||
  return downloadedFile;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<PackageInfo?> getInstalledInfo(String? packageName,
 | 
			
		||||
    {bool printErr = true}) async {
 | 
			
		||||
  if (packageName != null) {
 | 
			
		||||
@@ -493,13 +563,13 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        zipFile: File(filePath), destinationDir: Directory(destinationPath));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> installXApkDir(DownloadedXApkDir dir,
 | 
			
		||||
  Future<bool> installXApkDir(DownloadedXApkDir dir,
 | 
			
		||||
      {bool needsBGWorkaround = false}) async {
 | 
			
		||||
    // We don't know which APKs in an XAPK are supported by the user's device
 | 
			
		||||
    // So we try installing all of them and assume success if at least one installed
 | 
			
		||||
    // If 0 APKs installed, throw the first install error encountered
 | 
			
		||||
    var somethingInstalled = false;
 | 
			
		||||
    try {
 | 
			
		||||
      var somethingInstalled = false;
 | 
			
		||||
      MultiAppMultiError errors = MultiAppMultiError();
 | 
			
		||||
      for (var file in dir.extracted
 | 
			
		||||
          .listSync(recursive: true, followLinks: false)
 | 
			
		||||
@@ -526,6 +596,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dir.extracted.delete(recursive: true);
 | 
			
		||||
    }
 | 
			
		||||
    return somethingInstalled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> installApk(DownloadedApk file,
 | 
			
		||||
@@ -758,17 +829,18 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
        try {
 | 
			
		||||
          if (!skipInstalls) {
 | 
			
		||||
            bool sayInstalled = true;
 | 
			
		||||
            if (downloadedFile != null) {
 | 
			
		||||
              if (willBeSilent && context == null) {
 | 
			
		||||
                installApk(downloadedFile, needsBGWorkaround: true);
 | 
			
		||||
              } else {
 | 
			
		||||
                await installApk(downloadedFile);
 | 
			
		||||
                sayInstalled = await installApk(downloadedFile);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              if (willBeSilent && context == null) {
 | 
			
		||||
                installXApkDir(downloadedDir!, needsBGWorkaround: true);
 | 
			
		||||
              } else {
 | 
			
		||||
                await installXApkDir(downloadedDir!);
 | 
			
		||||
                sayInstalled = await installXApkDir(downloadedDir!);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            if (willBeSilent && context == null) {
 | 
			
		||||
@@ -776,7 +848,9 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
                  [apps[id]!.app],
 | 
			
		||||
                  id: id.hashCode));
 | 
			
		||||
            }
 | 
			
		||||
            installedIds.add(id);
 | 
			
		||||
            if (sayInstalled) {
 | 
			
		||||
              installedIds.add(id);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } finally {
 | 
			
		||||
          apps[id]?.downloadProgress = null;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -38,10 +38,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: app_links
 | 
			
		||||
      sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
 | 
			
		||||
      sha256: fd7fc1569870b4b0d90d17a9f36661a6ff92400fecb6e4adab4abe0f0488bb5f
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.5.1"
 | 
			
		||||
    version: "4.0.0"
 | 
			
		||||
  archive:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -307,10 +307,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications
 | 
			
		||||
      sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
 | 
			
		||||
      sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "16.3.3"
 | 
			
		||||
    version: "17.0.0"
 | 
			
		||||
  flutter_local_notifications_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -783,10 +783,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqflite_common
 | 
			
		||||
      sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
 | 
			
		||||
      sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.5.3"
 | 
			
		||||
    version: "2.5.4"
 | 
			
		||||
  stack_trace:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -959,10 +959,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_android
 | 
			
		||||
      sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
 | 
			
		||||
      sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.15.0"
 | 
			
		||||
    version: "3.16.0"
 | 
			
		||||
  webview_flutter_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -975,10 +975,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_wkwebview
 | 
			
		||||
      sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
 | 
			
		||||
      sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.12.0"
 | 
			
		||||
    version: "3.13.0"
 | 
			
		||||
  win32:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 1.0.5+2255 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 1.0.6+2256 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=3.0.0 <4.0.0'
 | 
			
		||||
@@ -38,7 +38,7 @@ dependencies:
 | 
			
		||||
  cupertino_icons: ^1.0.5
 | 
			
		||||
  path_provider: ^2.0.11
 | 
			
		||||
  flutter_fgbg: ^0.3.0 # Try removing reliance on this
 | 
			
		||||
  flutter_local_notifications: ^16.1.0
 | 
			
		||||
  flutter_local_notifications: ^17.0.0
 | 
			
		||||
  provider: ^6.0.3
 | 
			
		||||
  http: ^1.0.0
 | 
			
		||||
  webview_flutter: ^4.0.0
 | 
			
		||||
@@ -66,7 +66,7 @@ dependencies:
 | 
			
		||||
  connectivity_plus: ^5.0.0
 | 
			
		||||
  shared_storage: ^0.8.0
 | 
			
		||||
  crypto: ^3.0.3
 | 
			
		||||
  app_links: ^3.5.0
 | 
			
		||||
  app_links: ^4.0.0
 | 
			
		||||
  background_fetch: ^1.2.1
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user