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
							
								
								
									
										2
									
								
								.flutter
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						| Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 418 KiB | 
| Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 418 KiB | 
| Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 265 KiB | 
| Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 220 KiB | 
| Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 186 KiB | 
| Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 362 KiB | 
| @@ -501,7 +501,7 @@ class GitHub extends AppSource { | ||||
|   AppNames getAppNames(String standardUrl) { | ||||
|     String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); | ||||
|     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( | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class GitLab extends AppSource { | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { | ||||
|     RegExp standardUrlRegEx = RegExp( | ||||
|         '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', | ||||
|         '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^/]+){1,20}', | ||||
|         caseSensitive: false); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url); | ||||
|     if (match == null) { | ||||
| @@ -126,6 +126,8 @@ class GitLab extends AppSource { | ||||
|   ) async { | ||||
|     // Prepare request params | ||||
|     var names = GitHub().getAppNames(standardUrl); | ||||
|     String projectUriComponent = | ||||
|         '${Uri.encodeComponent(names.author)}%2F${Uri.encodeComponent(names.name)}'; | ||||
|     String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {}); | ||||
|     String optionalAuth = (PAT != null) ? 'private_token=$PAT' : ''; | ||||
|  | ||||
| @@ -133,7 +135,7 @@ class GitLab extends AppSource { | ||||
|  | ||||
|     // Get project ID | ||||
|     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); | ||||
|     if (res0.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res0); | ||||
| @@ -145,7 +147,7 @@ class GitLab extends AppSource { | ||||
|  | ||||
|     // Request data from REST API | ||||
|     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); | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
| @@ -180,7 +182,7 @@ class GitLab extends AppSource { | ||||
|       return APKDetails( | ||||
|           e['tag_name'] ?? e['name'], | ||||
|           getApkUrlsFromUrls(apkUrlsSet.toList()), | ||||
|           GitHub().getAppNames(standardUrl), | ||||
|           AppNames(names.author, names.name.split('/').last), | ||||
|           releaseDate: releaseDate); | ||||
|     }); | ||||
|     if (apkDetailsList.isEmpty) { | ||||
|   | ||||
| @@ -44,7 +44,8 @@ List<MapEntry<Locale, String>> supportedLocales = const [ | ||||
|   MapEntry(Locale('da'), 'Dansk'), | ||||
|   MapEntry(Locale('en', 'EO'), | ||||
|       '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 localeDir = 'assets/translations'; | ||||
| @@ -244,6 +245,7 @@ class _ObtainiumState extends State<Obtainium> { | ||||
|           supportedLocales: context.supportedLocales, | ||||
|           locale: context.locale, | ||||
|           navigatorKey: globalNavigatorKey, | ||||
|           debugShowCheckedModeBanner: false, | ||||
|           theme: ThemeData( | ||||
|               useMaterial3: true, | ||||
|               colorScheme: settingsProvider.theme == ThemeSettings.dark | ||||
|   | ||||
| @@ -151,13 +151,15 @@ Future<File> downloadFileWithRetry(String url, String fileName, | ||||
|     {bool useExisting = true, | ||||
|     Map<String, String>? headers, | ||||
|     int retries = 3, | ||||
|     bool allowInsecure = false}) async { | ||||
|     bool allowInsecure = false, | ||||
|     LogsProvider? logs}) async { | ||||
|   try { | ||||
|     return await downloadFile( | ||||
|         url, fileName, fileNameHasExt, onProgress, destDir, | ||||
|         useExisting: useExisting, | ||||
|         headers: headers, | ||||
|         allowInsecure: allowInsecure); | ||||
|         allowInsecure: allowInsecure, | ||||
|         logs: logs); | ||||
|   } catch (e) { | ||||
|     if (retries > 0 && e is ClientException) { | ||||
|       await Future.delayed(const Duration(seconds: 5)); | ||||
| @@ -166,7 +168,8 @@ Future<File> downloadFileWithRetry(String url, String fileName, | ||||
|           useExisting: useExisting, | ||||
|           headers: headers, | ||||
|           retries: (retries - 1), | ||||
|           allowInsecure: allowInsecure); | ||||
|           allowInsecure: allowInsecure, | ||||
|           logs: logs); | ||||
|     } else { | ||||
|       rethrow; | ||||
|     } | ||||
| @@ -219,7 +222,8 @@ Future<File> downloadFile(String url, String fileName, bool fileNameHasExt, | ||||
|     Function? onProgress, String destDir, | ||||
|     {bool useExisting = true, | ||||
|     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 | ||||
|   var reqHeaders = headers ?? {}; | ||||
|   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) | ||||
|   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), | ||||
|   // complete the already-started request, else cancel it and start a ranged request, | ||||
|   // 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) | ||||
|         var cutoff = DateTime.now().subtract(const Duration(days: 7)); | ||||
|         APKDir.listSync() | ||||
|             .where((element) => | ||||
|                 element.path.endsWith('.part') || | ||||
|                 element.statSync().modified.isBefore(cutoff)) | ||||
|             .where((element) => element.statSync().modified.isBefore(cutoff)) | ||||
|             .forEach((partialApk) { | ||||
|           if (!areDownloadsRunning()) { | ||||
|             partialApk.delete(recursive: true); | ||||
| @@ -495,7 +533,8 @@ class AppsProvider with ChangeNotifier { | ||||
|         prevProg = prog; | ||||
|       }, APKDir.path, | ||||
|           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' | ||||
|       if (apps[app.id] != null) { | ||||
|         apps[app.id]!.downloadProgress = -1; | ||||
| @@ -1124,7 +1163,8 @@ class AppsProvider with ChangeNotifier { | ||||
|                     forAPKDownload: | ||||
|                         fileUrl.key.endsWith('.apk') ? true : false), | ||||
|             useExisting: false, | ||||
|             allowInsecure: app.additionalSettings['allowInsecure'] == true); | ||||
|             allowInsecure: app.additionalSettings['allowInsecure'] == true, | ||||
|             logs: logs); | ||||
|         notificationsProvider | ||||
|             .notify(DownloadedNotification(fileUrl.key, fileUrl.value)); | ||||
|       } catch (e) { | ||||
| @@ -1414,8 +1454,10 @@ class AppsProvider with ChangeNotifier { | ||||
|         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; | ||||
|       } | ||||
|       if (!onlyIfExists || this.apps.containsKey(app.id)) { | ||||
|         File('${(await getAppsDir()).path}/${app.id}.json') | ||||
|             .writeAsStringSync(jsonEncode(app.toJson())); | ||||
|         String filePath = '${(await getAppsDir()).path}/${app.id}.json'; | ||||
|         File('$filePath.tmp') | ||||
|             .writeAsStringSync(jsonEncode(app.toJson())); // #2089 | ||||
|         File('$filePath.tmp').renameSync(filePath); | ||||
|       } | ||||
|       try { | ||||
|         this.apps.update(app.id, | ||||
|   | ||||
							
								
								
									
										36
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -224,10 +224,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dbus | ||||
|       sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" | ||||
|       sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.10" | ||||
|     version: "0.7.11" | ||||
|   device_info_plus: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -256,10 +256,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: easy_localization | ||||
|       sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 | ||||
|       sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.7" | ||||
|     version: "3.0.7+1" | ||||
|   easy_logger: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -405,10 +405,10 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_launcher_icons | ||||
|       sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" | ||||
|       sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.14.2" | ||||
|     version: "0.14.3" | ||||
|   flutter_lints: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -524,10 +524,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: http | ||||
|       sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 | ||||
|       sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.2" | ||||
|     version: "1.3.0" | ||||
|   http_parser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -844,18 +844,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a | ||||
|       sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.5" | ||||
|     version: "2.4.0" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84 | ||||
|       sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|     version: "2.4.4" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1155,10 +1155,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f | ||||
|       sha256: "5568f17a9c25c0fdd0737900fa1c2d1fee2d780bc212d9aec10c2d1f48ef0f59" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.3.0" | ||||
|     version: "4.3.1" | ||||
|   webview_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1171,18 +1171,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_wkwebview | ||||
|       sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7" | ||||
|       sha256: "8e0593559bfecd35eb1757d6907ed6b995a41ef82607d6113df897c2805ce6be" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.17.0" | ||||
|     version: "3.18.0" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" | ||||
|       sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.10.0" | ||||
|     version: "5.10.1" | ||||
|   win32_registry: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -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 | ||||
| # 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.1.39+2296 | ||||
| version: 1.1.40+2297 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.6.0 | ||||
|   | ||||