import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; extension Unique on List { List unique([Id Function(E element)? id, bool inplace = true]) { final ids = {}; var list = inplace ? this : List.from(this); list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); return list; } } class APKPure extends AppSource { APKPure() { hosts = ['apkpure.net', 'apkpure.com']; allowSubDomains = true; naiveStandardVersionDetection = true; showReleaseDateAsVersionToggle = true; additionalSourceAppSpecificSettingFormItems = [ [ GeneratedFormSwitch( 'fallbackToOlderReleases', label: tr('fallbackToOlderReleases'), defaultValue: true, ), ], [ GeneratedFormSwitch( 'stayOneVersionBehind', label: tr('stayOneVersionBehind'), defaultValue: false, ), ], [ GeneratedFormSwitch( 'useFirstApkOfVersion', label: tr('useFirstApkOfVersion'), defaultValue: true, ), ], ]; } @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegExB = RegExp( '^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+', caseSensitive: false, ); RegExpMatch? match = standardUrlRegExB.firstMatch(url); if (match != null) { var uri = Uri.parse(url); url = 'https://${uri.host.substring(2)}${uri.path}'; } RegExp standardUrlRegExA = RegExp( '^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+', caseSensitive: false, ); match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); } return match.group(0)!; } @override Future tryInferringAppId( String standardUrl, { Map additionalSettings = const {}, }) async { return Uri.parse(standardUrl).pathSegments.last; } Future getDetailsForVersion( List> versionVariants, List supportedArchs, Map additionalSettings, ) async { var apkUrls = versionVariants .map((e) { String appId = e['package_name']; String versionCode = e['version_code']; List architectures = e['native_code']?.cast(); String architectureString = architectures.join(','); if (architectures.contains("universal") || architectures.contains("unlimited")) { architectures = []; } if (additionalSettings['autoApkFilterByArch'] == true && architectures.isNotEmpty && architectures.where((a) => supportedArchs.contains(a)).isEmpty) { return null; } String type = e['asset']['type']; String downloadUri = e['asset']['url']; return MapEntry( '$appId-$versionCode-$architectureString.${type.toLowerCase()}', downloadUri, ); }) .nonNulls .toList() .unique((e) => e.key); if (apkUrls.isEmpty) { throw NoAPKError(); } // get version details from first variant var v = versionVariants.first; String version = v['version_name']; String author = v['developer']; String appName = v['title']; DateTime releaseDate = DateTime.parse(v['update_date']); String? changeLog = v['whatsnew']; if (changeLog != null && changeLog.isEmpty) { changeLog = null; } if (additionalSettings['useFirstApkOfVersion'] == true) { apkUrls = [apkUrls.first]; } return APKDetails( version, apkUrls, AppNames(author, appName), releaseDate: releaseDate, changeLog: changeLog, ); } @override Future?> getRequestHeaders( Map additionalSettings, { bool forAPKDownload = false, }) async { if (forAPKDownload) { return null; } else { return { "Ual-Access-Businessid": "projecta", "Ual-Access-ProjectA": '{"device_info":{"os_ver":"${((await DeviceInfoPlugin().androidInfo).version.sdkInt)}"}}', }; } } @override Future getLatestAPKDetails( String standardUrl, Map additionalSettings, ) async { String appId = (await tryInferringAppId(standardUrl))!; List supportedArchs = (await DeviceInfoPlugin().androidInfo).supportedAbis; // request versions from API var res = await sourceRequest( "https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en", additionalSettings, ); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } List> apks = jsonDecode( res.body, )['version_list'].cast>(); // group by version List>> versions = apks .fold>>>({}, ( Map>> val, Map element, ) { String v = element['version_name']; if (!val.containsKey(v)) { val[v] = []; } val[v]?.add(element); return val; }) .values .toList(); if (versions.isEmpty) { throw NoReleasesError(); } for (var i = 0; i < versions.length; i++) { var v = versions[i]; try { if (i == 0 && additionalSettings['stayOneVersionBehind'] == true) { throw NoReleasesError(); } return await getDetailsForVersion( v, supportedArchs, additionalSettings, ); } catch (e) { if (additionalSettings['fallbackToOlderReleases'] != true || i == versions.length - 1) { rethrow; } } } throw NoAPKError(); } }