mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 13:33:28 +01:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			v0.7.2-bet
			...
			v0.7.5-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 602f0c3bb2 | ||
|  | 00721e8ac4 | ||
|  | d19f9101d6 | ||
|  | a4bc278e4c | ||
|  | b04986622b | ||
|  | 2059e4fd44 | ||
|  | 618a1523cf | ||
|  | ba1cdc2c73 | ||
|  | aa2a25fffe | ||
|  | c8ec67aef3 | ||
|  | 9576a99a4e | 
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:html/parser.dart'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -28,41 +29,24 @@ class FDroid extends AppSource { | |||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => null; |   String? changeLogPageFromStandardUrl(String standardUrl) => null; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |   String? tryInferringAppId(String standardUrl) { | ||||||
|  |     return Uri.parse(standardUrl).pathSegments.last; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|   Future<APKDetails> getLatestAPKDetails( |       Response res, String apkUrlPrefix) { | ||||||
|       String standardUrl, List<String> additionalData) async { |  | ||||||
|     Response res = await get(Uri.parse(standardUrl)); |  | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       var releases = parse(res.body).querySelectorAll('.package-version'); |       List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; | ||||||
|       if (releases.isEmpty) { |       if (releases.isEmpty) { | ||||||
|         throw NoReleasesError(); |         throw NoReleasesError(); | ||||||
|       } |       } | ||||||
|       String? latestVersion = releases[0] |       String? latestVersion = releases[0]['versionName']; | ||||||
|           .querySelector('.package-version-header b') |  | ||||||
|           ?.innerHtml |  | ||||||
|           .split(' ') |  | ||||||
|           .sublist(1) |  | ||||||
|           .join(' '); |  | ||||||
|       if (latestVersion == null) { |       if (latestVersion == null) { | ||||||
|         throw NoVersionError(); |         throw NoVersionError(); | ||||||
|       } |       } | ||||||
|       List<String> apkUrls = releases |       List<String> apkUrls = releases | ||||||
|           .where((element) => |           .where((element) => element['versionName'] == latestVersion) | ||||||
|               element |           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') | ||||||
|                   .querySelector('.package-version-header b') |  | ||||||
|                   ?.innerHtml |  | ||||||
|                   .split(' ') |  | ||||||
|                   .sublist(1) |  | ||||||
|                   .join(' ') == |  | ||||||
|               latestVersion) |  | ||||||
|           .map((e) => |  | ||||||
|               e |  | ||||||
|                   .querySelector('.package-version-download a') |  | ||||||
|                   ?.attributes['href'] ?? |  | ||||||
|               '') |  | ||||||
|           .where((element) => element.isNotEmpty) |  | ||||||
|           .toList(); |           .toList(); | ||||||
|       if (apkUrls.isEmpty) { |       if (apkUrls.isEmpty) { | ||||||
|         throw NoAPKError(); |         throw NoAPKError(); | ||||||
| @@ -73,6 +57,15 @@ class FDroid extends AppSource { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<APKDetails> getLatestAPKDetails( | ||||||
|  |       String standardUrl, List<String> additionalData) async { | ||||||
|  |     String? appId = tryInferringAppId(standardUrl); | ||||||
|  |     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|  |         await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), | ||||||
|  |         'https://f-droid.org/repo/$appId'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   AppNames getAppNames(String standardUrl) { |   AppNames getAppNames(String standardUrl) { | ||||||
|     return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last); |     return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last); | ||||||
|   | |||||||
| @@ -105,9 +105,6 @@ class GitHub extends AppSource { | |||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => |   String? changeLogPageFromStandardUrl(String standardUrl) => | ||||||
|       '$standardUrl/releases'; |       '$standardUrl/releases'; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
| @@ -167,14 +164,8 @@ class GitHub extends AppSource { | |||||||
|       } |       } | ||||||
|       return APKDetails(version, targetRelease['apkUrls']); |       return APKDetails(version, targetRelease['apkUrls']); | ||||||
|     } else { |     } else { | ||||||
|       if (res.headers['x-ratelimit-remaining'] == '0') { |       rateLimitErrorCheck(res); | ||||||
|         throw RateLimitError( |       throw getObtainiumHttpError(res); | ||||||
|             (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / |  | ||||||
|                     60000000) |  | ||||||
|                 .round()); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       throw NoReleasesError(); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -200,15 +191,17 @@ class GitHub extends AppSource { | |||||||
|       } |       } | ||||||
|       return urlsWithDescriptions; |       return urlsWithDescriptions; | ||||||
|     } else { |     } else { | ||||||
|       if (res.headers['x-ratelimit-remaining'] == '0') { |       rateLimitErrorCheck(res); | ||||||
|         throw RateLimitError( |       throw getObtainiumHttpError(res); | ||||||
|             (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / |     } | ||||||
|                     60000000) |   } | ||||||
|                 .round()); |  | ||||||
|       } |   rateLimitErrorCheck(Response res) { | ||||||
|       throw ObtainiumError( |     if (res.headers['x-ratelimit-remaining'] == '0') { | ||||||
|           res.reasonPhrase ?? 'Error ${res.statusCode.toString()}', |       throw RateLimitError( | ||||||
|           unexpected: true); |           (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / | ||||||
|  |                   60000000) | ||||||
|  |               .round()); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,9 +23,6 @@ class GitLab extends AppSource { | |||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => |   String? changeLogPageFromStandardUrl(String standardUrl) => | ||||||
|       '$standardUrl/-/releases'; |       '$standardUrl/-/releases'; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import 'package:html/parser.dart'; |  | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
|  | import 'package:obtainium/app_sources/fdroid.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| @@ -22,41 +22,18 @@ class IzzyOnDroid extends AppSource { | |||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => null; |   String? changeLogPageFromStandardUrl(String standardUrl) => null; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |   String? tryInferringAppId(String standardUrl) { | ||||||
|  |     return FDroid().tryInferringAppId(standardUrl); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
|     Response res = await get(Uri.parse(standardUrl)); |     String? appId = tryInferringAppId(standardUrl); | ||||||
|     if (res.statusCode == 200) { |     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|       var parsedHtml = parse(res.body); |         await get( | ||||||
|       var multipleVersionApkUrls = parsedHtml |             Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')), | ||||||
|           .querySelectorAll('a') |         'https://android.izzysoft.de/frepo/$appId'); | ||||||
|           .where((element) => |  | ||||||
|               element.attributes['href']?.toLowerCase().endsWith('.apk') ?? |  | ||||||
|               false) |  | ||||||
|           .map((e) => 'https://$host${e.attributes['href'] ?? ''}') |  | ||||||
|           .toList(); |  | ||||||
|       if (multipleVersionApkUrls.isEmpty) { |  | ||||||
|         throw NoAPKError(); |  | ||||||
|       } |  | ||||||
|       var version = parsedHtml |  | ||||||
|           .querySelector('#keydata') |  | ||||||
|           ?.querySelectorAll('b') |  | ||||||
|           .where( |  | ||||||
|               (element) => element.innerHtml.toLowerCase().contains('version')) |  | ||||||
|           .toList()[0] |  | ||||||
|           .parentNode |  | ||||||
|           ?.parentNode |  | ||||||
|           ?.children[1] |  | ||||||
|           .innerHtml; |  | ||||||
|       if (version == null) { |  | ||||||
|         throw NoVersionError(); |  | ||||||
|       } |  | ||||||
|       return APKDetails(version, [multipleVersionApkUrls[0]]); |  | ||||||
|     } else { |  | ||||||
|       throw NoReleasesError(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -22,9 +22,6 @@ class Mullvad extends AppSource { | |||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => |   String? changeLogPageFromStandardUrl(String standardUrl) => | ||||||
|       'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md'; |       'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md'; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
|   | |||||||
| @@ -16,9 +16,6 @@ class Signal extends AppSource { | |||||||
|   @override |   @override | ||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => null; |   String? changeLogPageFromStandardUrl(String standardUrl) => null; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
|   | |||||||
| @@ -21,9 +21,6 @@ class SourceForge extends AppSource { | |||||||
|   @override |   @override | ||||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => null; |   String? changeLogPageFromStandardUrl(String standardUrl) => null; | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, List<String> additionalData) async { |       String standardUrl, List<String> additionalData) async { | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; | |||||||
|  |  | ||||||
| enum FormItemType { string, bool } | enum FormItemType { string, bool } | ||||||
|  |  | ||||||
| typedef OnValueChanges = void Function(List<String> values, bool valid); | typedef OnValueChanges = void Function( | ||||||
|  |     List<String> values, bool valid, bool isBuilding); | ||||||
|  |  | ||||||
| class GeneratedFormItem { | class GeneratedFormItem { | ||||||
|   late String label; |   late String label; | ||||||
| @@ -13,6 +14,7 @@ class GeneratedFormItem { | |||||||
|   late String id; |   late String id; | ||||||
|   late List<Widget> belowWidgets; |   late List<Widget> belowWidgets; | ||||||
|   late String? hint; |   late String? hint; | ||||||
|  |   late List<String>? opts; | ||||||
|  |  | ||||||
|   GeneratedFormItem( |   GeneratedFormItem( | ||||||
|       {this.label = 'Input', |       {this.label = 'Input', | ||||||
| @@ -22,7 +24,8 @@ class GeneratedFormItem { | |||||||
|       this.additionalValidators = const [], |       this.additionalValidators = const [], | ||||||
|       this.id = 'input', |       this.id = 'input', | ||||||
|       this.belowWidgets = const [], |       this.belowWidgets = const [], | ||||||
|       this.hint}); |       this.hint, | ||||||
|  |       this.opts}); | ||||||
| } | } | ||||||
|  |  | ||||||
| class GeneratedForm extends StatefulWidget { | class GeneratedForm extends StatefulWidget { | ||||||
| @@ -47,7 +50,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|   List<List<Widget>> rows = []; |   List<List<Widget>> rows = []; | ||||||
|  |  | ||||||
|   // If any value changes, call this to update the parent with value and validity |   // If any value changes, call this to update the parent with value and validity | ||||||
|   void someValueChanged() { |   void someValueChanged({bool isBuilding = false}) { | ||||||
|     List<String> returnValues = []; |     List<String> returnValues = []; | ||||||
|     var valid = true; |     var valid = true; | ||||||
|     for (int r = 0; r < values.length; r++) { |     for (int r = 0; r < values.length; r++) { | ||||||
| @@ -62,7 +65,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     widget.onValueChanges(returnValues, valid); |     widget.onValueChanges(returnValues, valid, isBuilding); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -75,14 +78,16 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|         .map((row) => row.map((e) { |         .map((row) => row.map((e) { | ||||||
|               return j < widget.defaultValues.length |               return j < widget.defaultValues.length | ||||||
|                   ? widget.defaultValues[j++] |                   ? widget.defaultValues[j++] | ||||||
|                   : ''; |                   : e.opts != null | ||||||
|  |                       ? e.opts!.first | ||||||
|  |                       : ''; | ||||||
|             }).toList()) |             }).toList()) | ||||||
|         .toList(); |         .toList(); | ||||||
|  |  | ||||||
|     // Dynamically create form inputs |     // Dynamically create form inputs | ||||||
|     formInputs = widget.items.asMap().entries.map((row) { |     formInputs = widget.items.asMap().entries.map((row) { | ||||||
|       return row.value.asMap().entries.map((e) { |       return row.value.asMap().entries.map((e) { | ||||||
|         if (e.value.type == FormItemType.string) { |         if (e.value.type == FormItemType.string && e.value.opts == null) { | ||||||
|           final formFieldKey = GlobalKey<FormFieldState>(); |           final formFieldKey = GlobalKey<FormFieldState>(); | ||||||
|           return TextFormField( |           return TextFormField( | ||||||
|             key: formFieldKey, |             key: formFieldKey, | ||||||
| @@ -112,11 +117,29 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|               return null; |               return null; | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|  |         } else if (e.value.type == FormItemType.string && | ||||||
|  |             e.value.opts != null) { | ||||||
|  |           if (e.value.opts!.isEmpty) { | ||||||
|  |             return const Text('ERROR: DROPDOWN MUST HAVE AT LEAST ONE OPT.'); | ||||||
|  |           } | ||||||
|  |           return DropdownButtonFormField( | ||||||
|  |               decoration: const InputDecoration(labelText: 'Colour'), | ||||||
|  |               value: values[row.key][e.key], | ||||||
|  |               items: e.value.opts! | ||||||
|  |                   .map((e) => DropdownMenuItem(value: e, child: Text(e))) | ||||||
|  |                   .toList(), | ||||||
|  |               onChanged: (value) { | ||||||
|  |                 setState(() { | ||||||
|  |                   values[row.key][e.key] = value ?? e.value.opts!.first; | ||||||
|  |                   someValueChanged(); | ||||||
|  |                 }); | ||||||
|  |               }); | ||||||
|         } else { |         } else { | ||||||
|           return Container(); // Some input types added in build |           return Container(); // Some input types added in build | ||||||
|         } |         } | ||||||
|       }).toList(); |       }).toList(); | ||||||
|     }).toList(); |     }).toList(); | ||||||
|  |     someValueChanged(isBuilding: true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -46,11 +46,16 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> { | |||||||
|           ), |           ), | ||||||
|         GeneratedForm( |         GeneratedForm( | ||||||
|             items: widget.items, |             items: widget.items, | ||||||
|             onValueChanges: (values, valid) { |             onValueChanges: (values, valid, isBuilding) { | ||||||
|               setState(() { |               if (isBuilding) { | ||||||
|                 this.values = values; |                 this.values = values; | ||||||
|                 this.valid = valid; |                 this.valid = valid; | ||||||
|               }); |               } else { | ||||||
|  |                 setState(() { | ||||||
|  |                   this.values = values; | ||||||
|  |                   this.valid = valid; | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|             }, |             }, | ||||||
|             defaultValues: widget.defaultValues) |             defaultValues: widget.defaultValues) | ||||||
|       ]), |       ]), | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import 'package:dynamic_color/dynamic_color.dart'; | |||||||
| import 'package:device_info_plus/device_info_plus.dart'; | import 'package:device_info_plus/device_info_plus.dart'; | ||||||
| import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.7.2'; | const String currentVersion = '0.7.5'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,14 +27,9 @@ class GitHubStars implements MassAppUrlSource { | |||||||
|       } |       } | ||||||
|       return urlsWithDescriptions; |       return urlsWithDescriptions; | ||||||
|     } else { |     } else { | ||||||
|       if (res.headers['x-ratelimit-remaining'] == '0') { |       var gh = GitHub(); | ||||||
|         throw RateLimitError( |       gh.rateLimitErrorCheck(res); | ||||||
|             (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / |       throw getObtainiumHttpError(res); | ||||||
|                     60000000) |  | ||||||
|                 .round()); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       throw ObtainiumError('Unable to find user\'s starred repos'); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                           ]) |                                           ]) | ||||||
|                                     ] |                                     ] | ||||||
|                                   ], |                                   ], | ||||||
|                                   onValueChanges: (values, valid) { |                                   onValueChanges: (values, valid, isBuilding) { | ||||||
|                                     setState(() { |                                     setState(() { | ||||||
|                                       userInput = values[0]; |                                       userInput = values[0]; | ||||||
|                                       var source = valid |                                       var source = valid | ||||||
| @@ -115,18 +115,23 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                                     additionalData); |                                                     additionalData); | ||||||
|                                             await settingsProvider |                                             await settingsProvider | ||||||
|                                                 .getInstallPermission(); |                                                 .getInstallPermission(); | ||||||
|                                             // ignore: use_build_context_synchronously |                                             // Only download the APK here if you need to for the package ID | ||||||
|                                             var apkUrl = await appsProvider |                                             if (sourceProvider | ||||||
|                                                 .confirmApkUrl(app, context); |                                                 .isTempId(app.id)) { | ||||||
|                                             if (apkUrl == null) { |                                               // ignore: use_build_context_synchronously | ||||||
|                                               throw ObtainiumError('Cancelled'); |                                               var apkUrl = await appsProvider | ||||||
|  |                                                   .confirmApkUrl(app, context); | ||||||
|  |                                               if (apkUrl == null) { | ||||||
|  |                                                 throw ObtainiumError( | ||||||
|  |                                                     'Cancelled'); | ||||||
|  |                                               } | ||||||
|  |                                               app.preferredApkIndex = | ||||||
|  |                                                   app.apkUrls.indexOf(apkUrl); | ||||||
|  |                                               var downloadedApk = | ||||||
|  |                                                   await appsProvider | ||||||
|  |                                                       .downloadApp(app); | ||||||
|  |                                               app.id = downloadedApk.appId; | ||||||
|                                             } |                                             } | ||||||
|                                             app.preferredApkIndex = |  | ||||||
|                                                 app.apkUrls.indexOf(apkUrl); |  | ||||||
|                                             var downloadedApk = |  | ||||||
|                                                 await appsProvider |  | ||||||
|                                                     .downloadApp(app); |  | ||||||
|                                             app.id = downloadedApk.appId; |  | ||||||
|                                             if (appsProvider.apps |                                             if (appsProvider.apps | ||||||
|                                                 .containsKey(app.id)) { |                                                 .containsKey(app.id)) { | ||||||
|                                               throw ObtainiumError( |                                               throw ObtainiumError( | ||||||
| @@ -174,7 +179,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                 .additionalDataFormItems.isNotEmpty) |                                 .additionalDataFormItems.isNotEmpty) | ||||||
|                               GeneratedForm( |                               GeneratedForm( | ||||||
|                                   items: pickedSource!.additionalDataFormItems, |                                   items: pickedSource!.additionalDataFormItems, | ||||||
|                                   onValueChanges: (values, valid) { |                                   onValueChanges: (values, valid, isBuilding) { | ||||||
|                                     setState(() { |                                     setState(() { | ||||||
|                                       additionalData = values; |                                       additionalData = values; | ||||||
|                                       validAdditionalData = valid; |                                       validAdditionalData = valid; | ||||||
|   | |||||||
| @@ -142,7 +142,7 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|       if (e.moreSourceSettingsFormItems.isNotEmpty) { |       if (e.moreSourceSettingsFormItems.isNotEmpty) { | ||||||
|         return GeneratedForm( |         return GeneratedForm( | ||||||
|             items: e.moreSourceSettingsFormItems.map((e) => [e]).toList(), |             items: e.moreSourceSettingsFormItems.map((e) => [e]).toList(), | ||||||
|             onValueChanges: (values, valid) { |             onValueChanges: (values, valid, isBuilding) { | ||||||
|               if (valid) { |               if (valid) { | ||||||
|                 for (var i = 0; i < values.length; i++) { |                 for (var i = 0; i < values.length; i++) { | ||||||
|                   settingsProvider.setSettingString( |                   settingsProvider.setSettingString( | ||||||
| @@ -264,27 +264,11 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                             if (logs.isEmpty) { |                             if (logs.isEmpty) { | ||||||
|                               showError(ObtainiumError('No Logs'), context); |                               showError(ObtainiumError('No Logs'), context); | ||||||
|                             } else { |                             } else { | ||||||
|                               String logString = |  | ||||||
|                                   logs.map((e) => e.toString()).join('\n\n'); |  | ||||||
|                               showDialog( |                               showDialog( | ||||||
|                                   context: context, |                                   context: context, | ||||||
|                                   builder: (BuildContext ctx) { |                                   builder: (BuildContext ctx) { | ||||||
|                                     return GeneratedFormModal( |                                     return const LogsDialog(); | ||||||
|                                       title: 'Obtainium App Logs', |                                   }); | ||||||
|                                       items: const [], |  | ||||||
|                                       defaultValues: const [], |  | ||||||
|                                       message: logString, |  | ||||||
|                                       initValid: true, |  | ||||||
|                                     ); |  | ||||||
|                                   }).then((value) { |  | ||||||
|                                 if (value != null) { |  | ||||||
|                                   Share.share( |  | ||||||
|                                       logs |  | ||||||
|                                           .map((e) => e.toString()) |  | ||||||
|                                           .join('\n\n'), |  | ||||||
|                                       subject: 'Obtainium App Logs'); |  | ||||||
|                                 } |  | ||||||
|                               }); |  | ||||||
|                             } |                             } | ||||||
|                           }); |                           }); | ||||||
|                         }, |                         }, | ||||||
| @@ -299,3 +283,71 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|         ])); |         ])); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class LogsDialog extends StatefulWidget { | ||||||
|  |   const LogsDialog({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<LogsDialog> createState() => _LogsDialogState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _LogsDialogState extends State<LogsDialog> { | ||||||
|  |   String? logString; | ||||||
|  |   List<int> days = [7, 5, 4, 3, 2, 1]; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     var logsProvider = context.read<LogsProvider>(); | ||||||
|  |     void filterLogs(int days) { | ||||||
|  |       logsProvider | ||||||
|  |           .get(after: DateTime.now().subtract(Duration(days: days))) | ||||||
|  |           .then((value) { | ||||||
|  |         setState(() { | ||||||
|  |           String l = value.map((e) => e.toString()).join('\n\n'); | ||||||
|  |           logString = l.isNotEmpty ? l : 'No Logs'; | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (logString == null) { | ||||||
|  |       filterLogs(days.first); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return AlertDialog( | ||||||
|  |       scrollable: true, | ||||||
|  |       title: const Text('Obtainium App Logs'), | ||||||
|  |       content: Column( | ||||||
|  |         children: [ | ||||||
|  |           DropdownButtonFormField( | ||||||
|  |               value: days.first, | ||||||
|  |               items: days | ||||||
|  |                   .map((e) => DropdownMenuItem( | ||||||
|  |                         value: e, | ||||||
|  |                         child: Text('$e Day${e == 1 ? '' : 's'}'), | ||||||
|  |                       )) | ||||||
|  |                   .toList(), | ||||||
|  |               onChanged: (d) { | ||||||
|  |                 filterLogs(d ?? 7); | ||||||
|  |               }), | ||||||
|  |           const SizedBox( | ||||||
|  |             height: 32, | ||||||
|  |           ), | ||||||
|  |           Text(logString ?? '') | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       actions: [ | ||||||
|  |         TextButton( | ||||||
|  |             onPressed: () { | ||||||
|  |               Navigator.of(context).pop(); | ||||||
|  |             }, | ||||||
|  |             child: const Text('Close')), | ||||||
|  |         TextButton( | ||||||
|  |             onPressed: () { | ||||||
|  |               Share.share(logString ?? '', subject: 'Obtainium App Logs'); | ||||||
|  |               Navigator.of(context).pop(); | ||||||
|  |             }, | ||||||
|  |             child: const Text('Share')) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -65,7 +65,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|         // Delete existing APKs |         // Delete existing APKs | ||||||
|         (await getExternalStorageDirectory()) |         (await getExternalStorageDirectory()) | ||||||
|             ?.listSync() |             ?.listSync() | ||||||
|             .where((element) => element.path.endsWith('.apk')) |             .where((element) => | ||||||
|  |                 element.path.endsWith('.apk') || | ||||||
|  |                 element.path.endsWith('.apk.part')) | ||||||
|             .forEach((apk) { |             .forEach((apk) { | ||||||
|           apk.delete(); |           apk.delete(); | ||||||
|         }); |         }); | ||||||
| @@ -73,38 +75,39 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   downloadFile(String url, String fileName, Function? onProgress) async { |   downloadFile(String url, String fileName, Function? onProgress, | ||||||
|  |       {bool useExisting = true}) async { | ||||||
|     var destDir = (await getExternalStorageDirectory())!.path; |     var destDir = (await getExternalStorageDirectory())!.path; | ||||||
|     StreamedResponse response = |     StreamedResponse response = | ||||||
|         await Client().send(Request('GET', Uri.parse(url))); |         await Client().send(Request('GET', Uri.parse(url))); | ||||||
|     File downloadedFile = File('$destDir/$fileName'); |     File downloadedFile = File('$destDir/$fileName'); | ||||||
|  |     if (!(downloadedFile.existsSync() && useExisting)) { | ||||||
|     if (downloadedFile.existsSync()) { |       File tempDownloadedFile = File('${downloadedFile.path}.part'); | ||||||
|       downloadedFile.deleteSync(); |       if (tempDownloadedFile.existsSync()) { | ||||||
|     } |         tempDownloadedFile.deleteSync(); | ||||||
|     var length = response.contentLength; |       } | ||||||
|     var received = 0; |       var length = response.contentLength; | ||||||
|     double? progress; |       var received = 0; | ||||||
|     var sink = downloadedFile.openWrite(); |       double? progress; | ||||||
|  |       var sink = tempDownloadedFile.openWrite(); | ||||||
|     await response.stream.map((s) { |       await response.stream.map((s) { | ||||||
|       received += s.length; |         received += s.length; | ||||||
|       progress = (length != null ? received / length * 100 : 30); |         progress = (length != null ? received / length * 100 : 30); | ||||||
|  |         if (onProgress != null) { | ||||||
|  |           onProgress(progress); | ||||||
|  |         } | ||||||
|  |         return s; | ||||||
|  |       }).pipe(sink); | ||||||
|  |       await sink.close(); | ||||||
|  |       progress = null; | ||||||
|       if (onProgress != null) { |       if (onProgress != null) { | ||||||
|         onProgress(progress); |         onProgress(progress); | ||||||
|       } |       } | ||||||
|       return s; |       if (response.statusCode != 200) { | ||||||
|     }).pipe(sink); |         tempDownloadedFile.deleteSync(); | ||||||
|  |         throw response.reasonPhrase ?? 'Unknown Error'; | ||||||
|     await sink.close(); |       } | ||||||
|     progress = null; |       tempDownloadedFile.renameSync(downloadedFile.path); | ||||||
|     if (onProgress != null) { |  | ||||||
|       onProgress(progress); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (response.statusCode != 200) { |  | ||||||
|       downloadedFile.deleteSync(); |  | ||||||
|       throw response.reasonPhrase ?? 'Unknown Error'; |  | ||||||
|     } |     } | ||||||
|     return downloadedFile; |     return downloadedFile; | ||||||
|   } |   } | ||||||
| @@ -424,22 +427,28 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|     loadingApps = true; |     loadingApps = true; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|     List<FileSystemEntity> appFiles = (await getAppsDir()) |     List<App> newApps = (await getAppsDir()) | ||||||
|         .listSync() |         .listSync() | ||||||
|         .where((item) => item.path.toLowerCase().endsWith('.json')) |         .where((item) => item.path.toLowerCase().endsWith('.json')) | ||||||
|  |         .map((e) => App.fromJson(jsonDecode(File(e.path).readAsStringSync()))) | ||||||
|         .toList(); |         .toList(); | ||||||
|     apps.clear(); |     var idsToDelete = apps.values | ||||||
|  |         .map((e) => e.app.id) | ||||||
|  |         .toSet() | ||||||
|  |         .difference(newApps.map((e) => e.id).toSet()); | ||||||
|  |     for (var id in idsToDelete) { | ||||||
|  |       apps.remove(id); | ||||||
|  |     } | ||||||
|     var sp = SourceProvider(); |     var sp = SourceProvider(); | ||||||
|     List<List<String>> errors = []; |     List<List<String>> errors = []; | ||||||
|     for (int i = 0; i < appFiles.length; i++) { |     for (int i = 0; i < newApps.length; i++) { | ||||||
|       App app = |       var info = await getInstalledInfo(newApps[i].id); | ||||||
|           App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync())); |  | ||||||
|       var info = await getInstalledInfo(app.id); |  | ||||||
|       try { |       try { | ||||||
|         sp.getSource(app.url); |         sp.getSource(newApps[i].url); | ||||||
|         apps.putIfAbsent(app.id, () => AppInMemory(app, null, info)); |         apps.putIfAbsent( | ||||||
|  |             newApps[i].id, () => AppInMemory(newApps[i], null, info)); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         errors.add([app.id, app.name, e.toString()]); |         errors.add([newApps[i].id, newApps[i].name, e.toString()]); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (errors.isNotEmpty) { |     if (errors.isNotEmpty) { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:html/dom.dart'; | import 'package:html/dom.dart'; | ||||||
|  | import 'package:http/http.dart'; | ||||||
| import 'package:obtainium/app_sources/fdroid.dart'; | import 'package:obtainium/app_sources/fdroid.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/app_sources/gitlab.dart'; | import 'package:obtainium/app_sources/gitlab.dart'; | ||||||
| @@ -154,14 +155,23 @@ class AppSource { | |||||||
|     throw NotImplementedError(); |     throw NotImplementedError(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> apkUrlPrefetchModifier(String apkUrl) { |   Future<String> apkUrlPrefetchModifier(String apkUrl) async { | ||||||
|     throw NotImplementedError(); |     return apkUrl; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool canSearch = false; |   bool canSearch = false; | ||||||
|   Future<Map<String, String>> search(String query) { |   Future<Map<String, String>> search(String query) { | ||||||
|     throw NotImplementedError(); |     throw NotImplementedError(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   String? tryInferringAppId(String standardUrl) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ObtainiumError getObtainiumHttpError(Response res) { | ||||||
|  |   return ObtainiumError( | ||||||
|  |       res.reasonPhrase ?? 'Error ${res.statusCode.toString()}'); | ||||||
| } | } | ||||||
|  |  | ||||||
| abstract class MassAppUrlSource { | abstract class MassAppUrlSource { | ||||||
| @@ -234,7 +244,9 @@ class SourceProvider { | |||||||
|     APKDetails apk = |     APKDetails apk = | ||||||
|         await source.getLatestAPKDetails(standardUrl, additionalData); |         await source.getLatestAPKDetails(standardUrl, additionalData); | ||||||
|     return App( |     return App( | ||||||
|         id ?? generateTempID(names, source), |         id ?? | ||||||
|  |             source.tryInferringAppId(standardUrl) ?? | ||||||
|  |             generateTempID(names, source), | ||||||
|         standardUrl, |         standardUrl, | ||||||
|         names.author[0].toUpperCase() + names.author.substring(1), |         names.author[0].toUpperCase() + names.author.substring(1), | ||||||
|         name.trim().isNotEmpty |         name.trim().isNotEmpty | ||||||
|   | |||||||
| @@ -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: 0.7.2+58 # When changing this, update the tag in main() accordingly | version: 0.7.5+61 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user