diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3e13a38..90ccc57 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ android:label="Obtainium" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" - android:requestLegacyExternalStorage="true"> + android:requestLegacyExternalStorage="true" + android:usesCleartextTraffic="true"> - - - - - - - reg.hasMatch(element.key)).toList(); + } if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { continue; } diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index 7ed84d6..45c2048 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -1,5 +1,4 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:obtainium/components/generated_form.dart'; @@ -100,28 +99,6 @@ class HTML extends AppSource { } ]) ], - [ - GeneratedFormTextField('versionExtractionRegEx', - label: tr('versionExtractionRegEx'), - required: false, - additionalValidators: [(value) => regExValidator(value)]), - ], - [ - GeneratedFormTextField('matchGroupToUse', - label: tr('matchGroupToUse'), - required: false, - hint: '0', - textInputType: const TextInputType.numberWithOptions(), - additionalValidators: [ - (value) { - if (value?.isEmpty == true) { - value = null; - } - value ??= '0'; - return intValidator(value); - } - ]) - ], [ GeneratedFormSwitch('versionExtractWholePage', label: tr('versionExtractWholePage')) @@ -242,9 +219,14 @@ class HTML extends AppSource { Map additionalSettings, ) async { var currentUrl = standardUrl; - for (int i = 0; - i < (additionalSettings['intermediateLink']?.length ?? 0); - i++) { + if (additionalSettings['intermediateLink']?.isNotEmpty != true) { + additionalSettings['intermediateLink'] = []; + } + additionalSettings['intermediateLink'] = + additionalSettings['intermediateLink'] + .where((l) => l['customLinkFilterRegex'].isNotEmpty == true) + .toList(); + for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { var intLinks = await grabLinksCommon(await sourceRequest(currentUrl), additionalSettings['intermediateLink'][i]); if (intLinks.isEmpty) { @@ -270,26 +252,12 @@ class HTML extends AppSource { if (additionalSettings['supportFixedAPKURL'] != true) { version = rel.hashCode.toString(); } - var versionExtractionRegEx = - additionalSettings['versionExtractionRegEx'] as String?; - if (versionExtractionRegEx?.isNotEmpty == true) { - var match = RegExp(versionExtractionRegEx!).allMatches( - additionalSettings['versionExtractWholePage'] == true - ? res.body.split('\r\n').join('\n').split('\n').join('\\n') - : rel); - if (match.isEmpty) { - throw NoVersionError(); - } - String matchGroupString = - (additionalSettings['matchGroupToUse'] as String).trim(); - if (matchGroupString.isEmpty) { - matchGroupString = "0"; - } - version = match.last.group(int.parse(matchGroupString)); - if (version?.isEmpty == true) { - throw NoVersionError(); - } - } + version = extractVersion( + additionalSettings['versionExtractionRegEx'] as String?, + additionalSettings['matchGroupToUse'] as String?, + additionalSettings['versionExtractWholePage'] == true + ? res.body.split('\r\n').join('\n').split('\n').join('\\n') + : rel); rel = ensureAbsoluteUrl(rel, uri); version ??= (await checkDownloadHash(rel)).toString(); return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(), diff --git a/lib/main.dart b/lib/main.dart index b8a8d8c..4ddb3e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.15.3'; +const String currentVersion = '0.15.4'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 0bba900..6d87a02 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -5,6 +5,7 @@ import 'package:app_links/app_links.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/pages/add_app.dart'; import 'package:obtainium/pages/apps.dart'; @@ -76,14 +77,39 @@ class _HomePageState extends State { try { if (action == 'add') { await goToAddApp(data); - } else if (action == 'app') { - await context - .read() - .import('{ "apps": [${Uri.decodeComponent(data)}] }'); - } else if (action == 'apps') { - await context - .read() - .import('{ "apps": ${Uri.decodeComponent(data)} }'); + } else if (action == 'app' || action == 'apps') { + var dataStr = Uri.decodeComponent(data); + if (await showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('importX', args: [ + action == 'app' ? tr('app') : tr('appsString') + ]), + items: const [], + additionalWidgets: [ + ExpansionTile( + title: const Text('Raw JSON'), + children: [ + Text( + dataStr, + style: const TextStyle(fontFamily: 'monospace'), + ) + ], + ) + ], + ); + }) != + null) { + // ignore: use_build_context_synchronously + var result = await context.read().import( + action == 'app' + ? '{ "apps": [$dataStr] }' + : '{ "apps": $dataStr }'); + // ignore: use_build_context_synchronously + showMessage( + tr('importedX', args: [plural('apps', result.key)]), context); + } } else { throw ObtainiumError(tr('unknown')); } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index ef0aa8d..b1c8591 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:html/dom.dart'; import 'package:http/http.dart'; import 'package:obtainium/app_sources/apkmirror.dart'; @@ -444,6 +445,19 @@ abstract class AppSource { label: tr('trackOnly'), ) ], + [ + GeneratedFormTextField('versionExtractionRegEx', + label: tr('versionExtractionRegEx'), + required: false, + additionalValidators: [(value) => regExValidator(value)]), + ], + [ + GeneratedFormTextField('matchGroupToUse', + label: tr('matchGroupToUse'), + required: false, + hint: '\$0', + textInputType: const TextInputType.numberWithOptions()) + ], [ GeneratedFormDropdown( 'versionDetection', @@ -580,6 +594,57 @@ bool isTempId(App app) { return RegExp('^[0-9]+\$').hasMatch(app.id); } +replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) { + if (RegExp('^\\d+\$').hasMatch(matchGroupString)) { + matchGroupString = '\$$matchGroupString'; + } + // Regular expression to match numbers in the input string + final numberRegex = RegExp(r'\$\d+'); + // Extract all numbers from the input string + final numbers = numberRegex.allMatches(matchGroupString); + if (numbers.isEmpty) { + // If no numbers found, return the original string + return null; + } + // Replace numbers with corresponding match groups + var outputString = matchGroupString; + for (final numberMatch in numbers) { + final number = numberMatch.group(0)!; + final matchGroup = match.group(int.parse(number.substring(1))) ?? ''; + // Check if the number is preceded by a single backslash + final isEscaped = outputString.contains('\\$number'); + // Replace the number with the corresponding match group + if (!isEscaped) { + outputString = outputString.replaceAll(number, matchGroup); + } else { + outputString = outputString.replaceAll('\\$number', number); + } + } + return outputString; +} + +String? extractVersion(String? versionExtractionRegEx, String? matchGroupString, + String stringToCheck) { + if (versionExtractionRegEx?.isNotEmpty == true) { + String? version = stringToCheck; + var match = RegExp(versionExtractionRegEx!).allMatches(version); + if (match.isEmpty) { + throw NoVersionError(); + } + matchGroupString = matchGroupString?.trim() ?? ''; + if (matchGroupString.isEmpty) { + matchGroupString = "0"; + } + version = replaceMatchGroupsInString(match.last, matchGroupString); + if (version?.isNotEmpty != true) { + throw NoVersionError(); + } + return version!; + } else { + return null; + } +} + class SourceProvider { // Add more source classes here so they are available via the service List get sources => [ @@ -679,6 +744,18 @@ class SourceProvider { String standardUrl = source.standardizeUrl(url); APKDetails apk = await source.getLatestAPKDetails(standardUrl, additionalSettings); + + if (source.runtimeType != HTML().runtimeType) { + // HTML does it separately + String? extractedVersion = extractVersion( + additionalSettings['versionExtractionRegEx'] as String?, + additionalSettings['matchGroupToUse'] as String?, + apk.version); + if (extractedVersion != null) { + apk.version = extractedVersion; + } + } + if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' && apk.releaseDate != null) { apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString(); diff --git a/pubspec.lock b/pubspec.lock index 5a31918..c1f6125 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -46,10 +46,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index da63a01..1ceb551 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.15.3+239 # When changing this, update the tag in main() accordingly +version: 0.15.4+240 # When changing this, update the tag in main() accordingly environment: sdk: '>=3.0.0 <4.0.0'