mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-19 05:00:21 +02: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
|
if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi
|
||||||
echo "beta=$BETA" >> $GITHUB_OUTPUT
|
echo "beta=$BETA" >> $GITHUB_OUTPUT
|
||||||
TAG="v$VERSION"
|
TAG="v$VERSION"
|
||||||
if [ $BETA == true ]; then TAG="$TAG"-beta; fi
|
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build APKs
|
- 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 localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
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')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
@@ -21,11 +22,6 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
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 keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
@@ -33,7 +29,8 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
namespace "dev.imranr.obtainium"
|
||||||
|
compileSdk flutter.compileSdkVersion
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -54,7 +51,7 @@ android {
|
|||||||
// You can update the following values to match your application needs.
|
// 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.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
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")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
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/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
|
import 'package:obtainium/pages/apps.dart';
|
||||||
import 'package:obtainium/pages/settings.dart';
|
import 'package:obtainium/pages/settings.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
@@ -108,6 +109,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
infoLines =
|
infoLines =
|
||||||
'$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}';
|
'$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(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -125,13 +127,26 @@ class _AppPageState extends State<AppPage> {
|
|||||||
.textTheme
|
.textTheme
|
||||||
.bodyLarge!
|
.bodyLarge!
|
||||||
.copyWith(fontWeight: FontWeight.bold)),
|
.copyWith(fontWeight: FontWeight.bold)),
|
||||||
app?.app.releaseDate == null
|
changeLogFn != null || app?.app.releaseDate != null
|
||||||
? const SizedBox.shrink()
|
? GestureDetector(
|
||||||
: Text(
|
onTap: changeLogFn,
|
||||||
app!.app.releaseDate.toString(),
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
app?.app.releaseDate == null
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
? 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(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
@@ -361,6 +376,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
!areDownloadsRunning
|
!areDownloadsRunning
|
||||||
? () async {
|
? () async {
|
||||||
try {
|
try {
|
||||||
|
var successMessage = app?.app.installedVersion == null
|
||||||
|
? tr('installed')
|
||||||
|
: tr('appsUpdated');
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||||
app?.app.id != null ? [app!.app.id] : [],
|
app?.app.id != null ? [app!.app.id] : [],
|
||||||
@@ -368,7 +386,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
);
|
);
|
||||||
if (res.isNotEmpty && !trackOnly) {
|
if (res.isNotEmpty && !trackOnly) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showMessage(tr('appsUpdated'), context);
|
showMessage(successMessage, context);
|
||||||
}
|
}
|
||||||
if (res.isNotEmpty && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@@ -26,6 +26,92 @@ class AppsPage extends StatefulWidget {
|
|||||||
State<AppsPage> createState() => AppsPageState();
|
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> {
|
class AppsPageState extends State<AppsPage> {
|
||||||
AppsFilter filter = AppsFilter();
|
AppsFilter filter = AppsFilter();
|
||||||
final AppsFilter neutralFilter = AppsFilter();
|
final AppsFilter neutralFilter = AppsFilter();
|
||||||
@@ -262,66 +348,6 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.where((a) => selectedAppIds.contains(a.id))
|
.where((a) => selectedAppIds.contains(a.id))
|
||||||
.toSet();
|
.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() {
|
getLoadingWidgets() {
|
||||||
return [
|
return [
|
||||||
if (listedApps.isEmpty)
|
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) {
|
getUpdateButton(int appIndex) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@@ -444,7 +441,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSingleAppHorizTile(int index) {
|
getSingleAppHorizTile(int index) {
|
||||||
var showChangesFn = getChangeLogFn(index);
|
var showChangesFn = getChangeLogFn(context, listedApps[index].app);
|
||||||
var hasUpdate = listedApps[index].app.installedVersion != null &&
|
var hasUpdate = listedApps[index].app.installedVersion != null &&
|
||||||
listedApps[index].app.installedVersion !=
|
listedApps[index].app.installedVersion !=
|
||||||
listedApps[index].app.latestVersion;
|
listedApps[index].app.latestVersion;
|
||||||
|
@@ -202,14 +202,18 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
|
|||||||
Future<File> downloadFile(
|
Future<File> downloadFile(
|
||||||
String url, String fileNameNoExt, Function? onProgress, String destDir,
|
String url, String fileNameNoExt, Function? onProgress, String destDir,
|
||||||
{bool useExisting = true, Map<String, String>? headers}) async {
|
{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));
|
var req = Request('GET', Uri.parse(url));
|
||||||
if (headers != null) {
|
req.headers.addAll(reqHeaders);
|
||||||
req.headers.addAll(headers);
|
|
||||||
}
|
|
||||||
var client = http.Client();
|
var client = http.Client();
|
||||||
StreamedResponse response = await client.send(req);
|
StreamedResponse response = await client.send(req);
|
||||||
String ext =
|
var resHeaders = response.headers;
|
||||||
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
|
||||||
|
// 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")) {
|
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||||
ext = ext.substring(0, ext.length - 1);
|
ext = ext.substring(0, ext.length - 1);
|
||||||
}
|
}
|
||||||
@@ -217,41 +221,107 @@ Future<File> downloadFile(
|
|||||||
ext = 'apk';
|
ext = 'apk';
|
||||||
}
|
}
|
||||||
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
|
||||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
bool rangeFeatureEnabled = false;
|
||||||
if (tempDownloadedFile.existsSync()) {
|
if (resHeaders['accept-ranges']?.isNotEmpty == true) {
|
||||||
tempDownloadedFile.deleteSync(recursive: true);
|
rangeFeatureEnabled =
|
||||||
}
|
resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes';
|
||||||
var length = response.contentLength;
|
}
|
||||||
var received = 0;
|
|
||||||
double? progress;
|
// If you have an existing file that is usable,
|
||||||
var sink = tempDownloadedFile.openWrite();
|
// decide whether you can use it (either return full or resume partial)
|
||||||
await response.stream.map((s) {
|
var fullContentLength = response.contentLength;
|
||||||
received += s.length;
|
if (useExisting && downloadedFile.existsSync()) {
|
||||||
progress = (length != null ? received / length * 100 : 30);
|
var length = downloadedFile.lengthSync();
|
||||||
if (onProgress != null) {
|
if (fullContentLength == null) {
|
||||||
onProgress(progress);
|
// Assume full
|
||||||
|
client.close();
|
||||||
|
return downloadedFile;
|
||||||
|
} else {
|
||||||
|
// Check if resume needed/possible
|
||||||
|
if (length == fullContentLength) {
|
||||||
|
client.close();
|
||||||
|
return downloadedFile;
|
||||||
}
|
}
|
||||||
return s;
|
if (length > fullContentLength) {
|
||||||
}).pipe(sink);
|
useExisting = false;
|
||||||
await sink.close();
|
}
|
||||||
progress = null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (onProgress != null) {
|
||||||
onProgress(progress);
|
onProgress(progress);
|
||||||
}
|
}
|
||||||
if (response.statusCode != 200) {
|
return s;
|
||||||
tempDownloadedFile.deleteSync(recursive: true);
|
}).pipe(sink);
|
||||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
await sink.close();
|
||||||
}
|
progress = null;
|
||||||
if (tempDownloadedFile.existsSync()) {
|
if (onProgress != null) {
|
||||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
onProgress(progress);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client.close();
|
|
||||||
}
|
}
|
||||||
|
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;
|
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,
|
Future<PackageInfo?> getInstalledInfo(String? packageName,
|
||||||
{bool printErr = true}) async {
|
{bool printErr = true}) async {
|
||||||
if (packageName != null) {
|
if (packageName != null) {
|
||||||
@@ -493,13 +563,13 @@ class AppsProvider with ChangeNotifier {
|
|||||||
zipFile: File(filePath), destinationDir: Directory(destinationPath));
|
zipFile: File(filePath), destinationDir: Directory(destinationPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installXApkDir(DownloadedXApkDir dir,
|
Future<bool> installXApkDir(DownloadedXApkDir dir,
|
||||||
{bool needsBGWorkaround = false}) async {
|
{bool needsBGWorkaround = false}) async {
|
||||||
// We don't know which APKs in an XAPK are supported by the user's device
|
// 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
|
// 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
|
// If 0 APKs installed, throw the first install error encountered
|
||||||
|
var somethingInstalled = false;
|
||||||
try {
|
try {
|
||||||
var somethingInstalled = false;
|
|
||||||
MultiAppMultiError errors = MultiAppMultiError();
|
MultiAppMultiError errors = MultiAppMultiError();
|
||||||
for (var file in dir.extracted
|
for (var file in dir.extracted
|
||||||
.listSync(recursive: true, followLinks: false)
|
.listSync(recursive: true, followLinks: false)
|
||||||
@@ -526,6 +596,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
} finally {
|
} finally {
|
||||||
dir.extracted.delete(recursive: true);
|
dir.extracted.delete(recursive: true);
|
||||||
}
|
}
|
||||||
|
return somethingInstalled;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installApk(DownloadedApk file,
|
Future<bool> installApk(DownloadedApk file,
|
||||||
@@ -758,17 +829,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
if (!skipInstalls) {
|
if (!skipInstalls) {
|
||||||
|
bool sayInstalled = true;
|
||||||
if (downloadedFile != null) {
|
if (downloadedFile != null) {
|
||||||
if (willBeSilent && context == null) {
|
if (willBeSilent && context == null) {
|
||||||
installApk(downloadedFile, needsBGWorkaround: true);
|
installApk(downloadedFile, needsBGWorkaround: true);
|
||||||
} else {
|
} else {
|
||||||
await installApk(downloadedFile);
|
sayInstalled = await installApk(downloadedFile);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (willBeSilent && context == null) {
|
if (willBeSilent && context == null) {
|
||||||
installXApkDir(downloadedDir!, needsBGWorkaround: true);
|
installXApkDir(downloadedDir!, needsBGWorkaround: true);
|
||||||
} else {
|
} else {
|
||||||
await installXApkDir(downloadedDir!);
|
sayInstalled = await installXApkDir(downloadedDir!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (willBeSilent && context == null) {
|
if (willBeSilent && context == null) {
|
||||||
@@ -776,7 +848,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
[apps[id]!.app],
|
[apps[id]!.app],
|
||||||
id: id.hashCode));
|
id: id.hashCode));
|
||||||
}
|
}
|
||||||
installedIds.add(id);
|
if (sayInstalled) {
|
||||||
|
installedIds.add(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
apps[id]?.downloadProgress = null;
|
apps[id]?.downloadProgress = null;
|
||||||
|
20
pubspec.lock
20
pubspec.lock
@@ -38,10 +38,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
|
sha256: fd7fc1569870b4b0d90d17a9f36661a6ff92400fecb6e4adab4abe0f0488bb5f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.5.1"
|
version: "4.0.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -307,10 +307,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
|
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.3.3"
|
version: "17.0.0"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -783,10 +783,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "2.5.4"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -959,10 +959,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
|
sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.15.0"
|
version: "3.16.0"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -975,10 +975,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
|
sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.12.0"
|
version: "3.13.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
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
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 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:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -38,7 +38,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.5
|
cupertino_icons: ^1.0.5
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
flutter_fgbg: ^0.3.0 # Try removing reliance on this
|
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
|
provider: ^6.0.3
|
||||||
http: ^1.0.0
|
http: ^1.0.0
|
||||||
webview_flutter: ^4.0.0
|
webview_flutter: ^4.0.0
|
||||||
@@ -66,7 +66,7 @@ dependencies:
|
|||||||
connectivity_plus: ^5.0.0
|
connectivity_plus: ^5.0.0
|
||||||
shared_storage: ^0.8.0
|
shared_storage: ^0.8.0
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
app_links: ^3.5.0
|
app_links: ^4.0.0
|
||||||
background_fetch: ^1.2.1
|
background_fetch: ^1.2.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user