Merge pull request #2094 from ImranR98/dev

- Attempt to fix "double download" bug (#2073)
- Avoid JSON corruption when storage full (#2089)
- Fix error when GitLab repo is in a subgroup (#2079)
- Update screenshots in README
This commit is contained in:
Imran
2025-01-27 21:13:54 -05:00
committed by GitHub
13 changed files with 83 additions and 37 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 362 KiB

View File

@@ -501,7 +501,7 @@ class GitHub extends AppSource {
AppNames getAppNames(String standardUrl) { AppNames getAppNames(String standardUrl) {
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/'); List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
return AppNames(names[0], names[1]); return AppNames(names[0], names.sublist(1).join('/'));
} }
Future<Map<String, List<String>>> searchCommon( Future<Map<String, List<String>>> searchCommon(

View File

@@ -54,7 +54,7 @@ class GitLab extends AppSource {
@override @override
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
RegExp standardUrlRegEx = RegExp( RegExp standardUrlRegEx = RegExp(
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^/]+){1,20}',
caseSensitive: false); caseSensitive: false);
RegExpMatch? match = standardUrlRegEx.firstMatch(url); RegExpMatch? match = standardUrlRegEx.firstMatch(url);
if (match == null) { if (match == null) {
@@ -126,6 +126,8 @@ class GitLab extends AppSource {
) async { ) async {
// Prepare request params // Prepare request params
var names = GitHub().getAppNames(standardUrl); var names = GitHub().getAppNames(standardUrl);
String projectUriComponent =
'${Uri.encodeComponent(names.author)}%2F${Uri.encodeComponent(names.name)}';
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {}); String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : ''; String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
@@ -133,7 +135,7 @@ class GitLab extends AppSource {
// Get project ID // Get project ID
Response res0 = await sourceRequest( Response res0 = await sourceRequest(
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}?$optionalAuth', 'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth',
additionalSettings); additionalSettings);
if (res0.statusCode != 200) { if (res0.statusCode != 200) {
throw getObtainiumHttpError(res0); throw getObtainiumHttpError(res0);
@@ -145,7 +147,7 @@ class GitLab extends AppSource {
// Request data from REST API // Request data from REST API
Response res = await sourceRequest( Response res = await sourceRequest(
'https://${hosts[0]}/api/v4/projects/${names.author}%2F${names.name}/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth', 'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
additionalSettings); additionalSettings);
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
@@ -180,7 +182,7 @@ class GitLab extends AppSource {
return APKDetails( return APKDetails(
e['tag_name'] ?? e['name'], e['tag_name'] ?? e['name'],
getApkUrlsFromUrls(apkUrlsSet.toList()), getApkUrlsFromUrls(apkUrlsSet.toList()),
GitHub().getAppNames(standardUrl), AppNames(names.author, names.name.split('/').last),
releaseDate: releaseDate); releaseDate: releaseDate);
}); });
if (apkDetailsList.isEmpty) { if (apkDetailsList.isEmpty) {

View File

@@ -44,7 +44,8 @@ List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('da'), 'Dansk'), MapEntry(Locale('da'), 'Dansk'),
MapEntry(Locale('en', 'EO'), MapEntry(Locale('en', 'EO'),
'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493 'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
MapEntry(Locale('in'), 'Bahasa Indonesia') MapEntry(Locale('in'), 'Bahasa Indonesia'),
MapEntry(Locale('ko'), '한국어'),
]; ];
const fallbackLocale = Locale('en'); const fallbackLocale = Locale('en');
const localeDir = 'assets/translations'; const localeDir = 'assets/translations';
@@ -244,6 +245,7 @@ class _ObtainiumState extends State<Obtainium> {
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
navigatorKey: globalNavigatorKey, navigatorKey: globalNavigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: settingsProvider.theme == ThemeSettings.dark colorScheme: settingsProvider.theme == ThemeSettings.dark

View File

@@ -151,13 +151,15 @@ Future<File> downloadFileWithRetry(String url, String fileName,
{bool useExisting = true, {bool useExisting = true,
Map<String, String>? headers, Map<String, String>? headers,
int retries = 3, int retries = 3,
bool allowInsecure = false}) async { bool allowInsecure = false,
LogsProvider? logs}) async {
try { try {
return await downloadFile( return await downloadFile(
url, fileName, fileNameHasExt, onProgress, destDir, url, fileName, fileNameHasExt, onProgress, destDir,
useExisting: useExisting, useExisting: useExisting,
headers: headers, headers: headers,
allowInsecure: allowInsecure); allowInsecure: allowInsecure,
logs: logs);
} catch (e) { } catch (e) {
if (retries > 0 && e is ClientException) { if (retries > 0 && e is ClientException) {
await Future.delayed(const Duration(seconds: 5)); await Future.delayed(const Duration(seconds: 5));
@@ -166,7 +168,8 @@ Future<File> downloadFileWithRetry(String url, String fileName,
useExisting: useExisting, useExisting: useExisting,
headers: headers, headers: headers,
retries: (retries - 1), retries: (retries - 1),
allowInsecure: allowInsecure); allowInsecure: allowInsecure,
logs: logs);
} else { } else {
rethrow; rethrow;
} }
@@ -219,7 +222,8 @@ Future<File> downloadFile(String url, String fileName, bool fileNameHasExt,
Function? onProgress, String destDir, Function? onProgress, String destDir,
{bool useExisting = true, {bool useExisting = true,
Map<String, String>? headers, Map<String, String>? headers,
bool allowInsecure = false}) async { bool allowInsecure = false,
LogsProvider? logs}) async {
// Send the initial request but cancel it as soon as you have the headers // Send the initial request but cancel it as soon as you have the headers
var reqHeaders = headers ?? {}; var reqHeaders = headers ?? {};
var req = Request('GET', Uri.parse(url)); var req = Request('GET', Uri.parse(url));
@@ -280,6 +284,42 @@ Future<File> downloadFile(String url, String fileName, bool fileNameHasExt,
// Download to a '.temp' file (to distinguish btn. complete/incomplete files) // Download to a '.temp' file (to distinguish btn. complete/incomplete files)
File tempDownloadedFile = File('${downloadedFile.path}.part'); File tempDownloadedFile = File('${downloadedFile.path}.part');
// If there is already a temp file, a download may already be in progress - account for this (see #2073)
bool tempFileExists = tempDownloadedFile.existsSync();
if (tempFileExists && useExisting) {
logs?.add(
'Partial download exists - will wait: ${tempDownloadedFile.uri.pathSegments.last}');
bool isDownloading = true;
int currentTempFileSize = await tempDownloadedFile.length();
bool shouldReturn = false;
while (isDownloading) {
await Future.delayed(Duration(seconds: 7));
if (tempDownloadedFile.existsSync()) {
int newTempFileSize = await tempDownloadedFile.length();
if (newTempFileSize > currentTempFileSize) {
currentTempFileSize = newTempFileSize;
logs?.add(
'Existing partial download still in progress: ${tempDownloadedFile.uri.pathSegments.last}');
} else {
logs?.add(
'Ignoring existing partial download: ${tempDownloadedFile.uri.pathSegments.last}');
break;
}
} else {
shouldReturn = downloadedFile.existsSync();
}
}
if (shouldReturn) {
logs?.add(
'Existing partial download completed - not repeating: ${tempDownloadedFile.uri.pathSegments.last}');
client.close();
return downloadedFile;
} else {
logs?.add(
'Existing partial download not in progress: ${tempDownloadedFile.uri.pathSegments.last}');
}
}
// If the range feature is not available (or you need to start a ranged req from 0), // 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, // complete the already-started request, else cancel it and start a ranged request,
// and open the file for writing in the appropriate mode // and open the file for writing in the appropriate mode
@@ -419,9 +459,7 @@ class AppsProvider with ChangeNotifier {
// Delete any partial APKs (if safe to do so) // Delete any partial APKs (if safe to do so)
var cutoff = DateTime.now().subtract(const Duration(days: 7)); var cutoff = DateTime.now().subtract(const Duration(days: 7));
APKDir.listSync() APKDir.listSync()
.where((element) => .where((element) => element.statSync().modified.isBefore(cutoff))
element.path.endsWith('.part') ||
element.statSync().modified.isBefore(cutoff))
.forEach((partialApk) { .forEach((partialApk) {
if (!areDownloadsRunning()) { if (!areDownloadsRunning()) {
partialApk.delete(recursive: true); partialApk.delete(recursive: true);
@@ -495,7 +533,8 @@ class AppsProvider with ChangeNotifier {
prevProg = prog; prevProg = prog;
}, APKDir.path, }, APKDir.path,
useExisting: useExisting, useExisting: useExisting,
allowInsecure: app.additionalSettings['allowInsecure'] == true); allowInsecure: app.additionalSettings['allowInsecure'] == true,
logs: logs);
// Set to 90 for remaining steps, will make null in 'finally' // Set to 90 for remaining steps, will make null in 'finally'
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = -1; apps[app.id]!.downloadProgress = -1;
@@ -1124,7 +1163,8 @@ class AppsProvider with ChangeNotifier {
forAPKDownload: forAPKDownload:
fileUrl.key.endsWith('.apk') ? true : false), fileUrl.key.endsWith('.apk') ? true : false),
useExisting: false, useExisting: false,
allowInsecure: app.additionalSettings['allowInsecure'] == true); allowInsecure: app.additionalSettings['allowInsecure'] == true,
logs: logs);
notificationsProvider notificationsProvider
.notify(DownloadedNotification(fileUrl.key, fileUrl.value)); .notify(DownloadedNotification(fileUrl.key, fileUrl.value));
} catch (e) { } catch (e) {
@@ -1414,8 +1454,10 @@ class AppsProvider with ChangeNotifier {
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
} }
if (!onlyIfExists || this.apps.containsKey(app.id)) { if (!onlyIfExists || this.apps.containsKey(app.id)) {
File('${(await getAppsDir()).path}/${app.id}.json') String filePath = '${(await getAppsDir()).path}/${app.id}.json';
.writeAsStringSync(jsonEncode(app.toJson())); File('$filePath.tmp')
.writeAsStringSync(jsonEncode(app.toJson())); // #2089
File('$filePath.tmp').renameSync(filePath);
} }
try { try {
this.apps.update(app.id, this.apps.update(app.id,

View File

@@ -224,10 +224,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dbus name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -256,10 +256,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: easy_localization name: easy_localization
sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7+1"
easy_logger: easy_logger:
dependency: transitive dependency: transitive
description: description:
@@ -405,10 +405,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.14.2" version: "0.14.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -524,10 +524,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.3.0"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@@ -844,18 +844,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.4.0"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84 sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.4"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -1155,10 +1155,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f sha256: "5568f17a9c25c0fdd0737900fa1c2d1fee2d780bc212d9aec10c2d1f48ef0f59"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1171,18 +1171,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7" sha256: "8e0593559bfecd35eb1757d6907ed6b995a41ef82607d6113df897c2805ce6be"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.17.0" version: "3.18.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.10.0" version: "5.10.1"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

View File

@@ -16,7 +16,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.1.39+2296 version: 1.1.40+2297
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0