diff --git a/lib/app_sources/apkcombo.dart b/lib/app_sources/apkcombo.dart index b63afa6..71fc596 100644 --- a/lib/app_sources/apkcombo.dart +++ b/lib/app_sources/apkcombo.dart @@ -12,8 +12,9 @@ class APKCombo extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+', + caseSensitive: false, + ); var match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -22,25 +23,30 @@ class APKCombo extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return Uri.parse(standardUrl).pathSegments.last; } @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { return { "User-Agent": "curl/8.0.1", "Accept": "*/*", "Connection": "keep-alive", - "Host": hosts[0] + "Host": hosts[0], }; } Future>> getApkUrls( - String standardUrl, Map additionalSettings) async { + String standardUrl, + Map additionalSettings, + ) async { var res = await sourceRequest('$standardUrl/download/apk', {}); if (res.statusCode != 200) { throw getObtainiumHttpError(res); @@ -65,7 +71,9 @@ class APKCombo extends AppSource { String verCode = e.querySelector('.info .header .vercode')?.text.trim() ?? ''; return MapEntry( - arch != null ? '$arch-$verCode.apk' : '', url ?? ''); + arch != null ? '$arch-$verCode.apk' : '', + url ?? '', + ); }).toList(); }) .reduce((value, element) => [...value, ...element]) @@ -74,8 +82,11 @@ class APKCombo extends AppSource { } @override - Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, - Map additionalSettings) async { + Future apkUrlPrefetchModifier( + String apkUrl, + String standardUrl, + Map additionalSettings, + ) async { var freshURLs = await getApkUrls(standardUrl, additionalSettings); var path2Match = Uri.parse(apkUrl).path; for (var url in freshURLs) { @@ -116,9 +127,10 @@ class APKCombo extends AppSource { } } return APKDetails( - version, - await getApkUrls(standardUrl, additionalSettings), - AppNames(author, appName), - releaseDate: releaseDate); + version, + await getApkUrls(standardUrl, additionalSettings), + AppNames(author, appName), + releaseDate: releaseDate, + ); } } diff --git a/lib/app_sources/apkmirror.dart b/lib/app_sources/apkmirror.dart index ad398e8..11c4fa3 100644 --- a/lib/app_sources/apkmirror.dart +++ b/lib/app_sources/apkmirror.dart @@ -17,37 +17,44 @@ class APKMirror extends AppSource { additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSwitch('fallbackToOlderReleases', - label: tr('fallbackToOlderReleases'), defaultValue: true) + GeneratedFormSwitch( + 'fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + defaultValue: true, + ), ], [ - GeneratedFormTextField('filterReleaseTitlesByRegEx', - label: tr('filterReleaseTitlesByRegEx'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) - ] + GeneratedFormTextField( + 'filterReleaseTitlesByRegEx', + label: tr('filterReleaseTitlesByRegEx'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), + ], ]; } @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { return { "User-Agent": - "Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}" + "Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}", }; } @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -68,12 +75,14 @@ class APKMirror extends AppSource { additionalSettings['fallbackToOlderReleases'] == true; String? regexFilter = (additionalSettings['filterReleaseTitlesByRegEx'] as String?) - ?.isNotEmpty == - true - ? additionalSettings['filterReleaseTitlesByRegEx'] - : null; - Response res = - await sourceRequest('$standardUrl/feed/', additionalSettings); + ?.isNotEmpty == + true + ? additionalSettings['filterReleaseTitlesByRegEx'] + : null; + Response res = await sourceRequest( + '$standardUrl/feed/', + additionalSettings, + ); if (res.statusCode == 200) { var items = parse(res.body).querySelectorAll('item'); dynamic targetRelease; @@ -95,11 +104,14 @@ class APKMirror extends AppSource { .split(' ') .sublist(0, 5) .join(' '); - DateTime? releaseDate = - dateString != null ? HttpDate.parse('$dateString GMT') : null; + DateTime? releaseDate = dateString != null + ? HttpDate.parse('$dateString GMT') + : null; String? version = titleString - ?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0, - RegExp(' by ').allMatches(titleString).last.start) + ?.substring( + RegExp('[0-9]').firstMatch(titleString)?.start ?? 0, + RegExp(' by ').allMatches(titleString).last.start, + ) .trim(); if (version == null || version.isEmpty) { version = titleString; @@ -107,8 +119,12 @@ class APKMirror extends AppSource { if (version == null || version.isEmpty) { throw NoVersionError(); } - return APKDetails(version, [], getAppNames(standardUrl), - releaseDate: releaseDate); + return APKDetails( + version, + [], + getAppNames(standardUrl), + releaseDate: releaseDate, + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart index e2c5a9c..888ab51 100644 --- a/lib/app_sources/apkpure.dart +++ b/lib/app_sources/apkpure.dart @@ -23,33 +23,44 @@ class APKPure extends AppSource { showReleaseDateAsVersionToggle = true; additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSwitch('fallbackToOlderReleases', - label: tr('fallbackToOlderReleases'), defaultValue: true) + GeneratedFormSwitch( + 'fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + defaultValue: true, + ), ], [ - GeneratedFormSwitch('stayOneVersionBehind', - label: tr('stayOneVersionBehind'), defaultValue: false) + GeneratedFormSwitch( + 'stayOneVersionBehind', + label: tr('stayOneVersionBehind'), + defaultValue: false, + ), ], [ - GeneratedFormSwitch('useFirstApkOfVersion', - label: tr('useFirstApkOfVersion'), defaultValue: true) - ] + 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); + '^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); + '^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+', + caseSensitive: false, + ); match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -58,15 +69,18 @@ class APKPure extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return Uri.parse(standardUrl).pathSegments.last; } getDetailsForVersion( - List> versionVariants, - List supportedArchs, - Map additionalSettings) async { + List> versionVariants, + List supportedArchs, + Map additionalSettings, + ) async { var apkUrls = versionVariants .map((e) { String appId = e['package_name']; @@ -88,8 +102,9 @@ class APKPure extends AppSource { String downloadUri = e['asset']['url']; return MapEntry( - '$appId-$versionCode-$architectureString.${type.toLowerCase()}', - downloadUri); + '$appId-$versionCode-$architectureString.${type.toLowerCase()}', + downloadUri, + ); }) .nonNulls .toList() @@ -114,14 +129,20 @@ class APKPure extends AppSource { apkUrls = [apkUrls.first]; } - return APKDetails(version, apkUrls, AppNames(author, appName), - releaseDate: releaseDate, changeLog: changeLog); + return APKDetails( + version, + apkUrls, + AppNames(author, appName), + releaseDate: releaseDate, + changeLog: changeLog, + ); } @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { if (forAPKDownload) { return null; } else { @@ -145,19 +166,22 @@ class APKPure extends AppSource { // request versions from API var res = await sourceRequest( - "https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en", - additionalSettings); + "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>(); + List> apks = jsonDecode( + res.body, + )['version_list'].cast>(); // group by version List>> versions = apks - .fold>>>({}, - (Map>> val, - Map element) { + .fold>>>({}, ( + Map>> val, + Map element, + ) { String v = element['version_name']; if (!val.containsKey(v)) { val[v] = []; @@ -179,7 +203,10 @@ class APKPure extends AppSource { throw NoReleasesError(); } return await getDetailsForVersion( - v, supportedArchs, additionalSettings); + v, + supportedArchs, + additionalSettings, + ); } catch (e) { if (additionalSettings['fallbackToOlderReleases'] != true || i == versions.length - 1) { diff --git a/lib/app_sources/aptoide.dart b/lib/app_sources/aptoide.dart index b545d8a..7a1ee5b 100644 --- a/lib/app_sources/aptoide.dart +++ b/lib/app_sources/aptoide.dart @@ -16,8 +16,9 @@ class Aptoide extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', - caseSensitive: false); + '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -26,14 +27,20 @@ class Aptoide extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return (await getAppDetailsJSON( - standardUrl, additionalSettings))['package']; + standardUrl, + additionalSettings, + ))['package']; } Future> getAppDetailsJSON( - String standardUrl, Map additionalSettings) async { + String standardUrl, + Map additionalSettings, + ) async { var res = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); @@ -46,7 +53,9 @@ class Aptoide extends AppSource { throw NoReleasesError(); } var res2 = await sourceRequest( - 'https://ws2.aptoide.com/api/7/getApp/app_id/$id', additionalSettings); + 'https://ws2.aptoide.com/api/7/getApp/app_id/$id', + additionalSettings, + ); if (res2.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -76,7 +85,10 @@ class Aptoide extends AppSource { } return APKDetails( - version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), - releaseDate: relDate); + version, + getApkUrlsFromUrls([apkUrl]), + AppNames(author, appName), + releaseDate: relDate, + ); } } diff --git a/lib/app_sources/codeberg.dart b/lib/app_sources/codeberg.dart index e2899c3..d54cfe5 100644 --- a/lib/app_sources/codeberg.dart +++ b/lib/app_sources/codeberg.dart @@ -18,8 +18,9 @@ class Codeberg extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -36,8 +37,9 @@ class Codeberg extends AppSource { String standardUrl, Map additionalSettings, ) async { - return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings, - (bool useTagUrl) async { + return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings, ( + bool useTagUrl, + ) async { return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100'; }, null); } @@ -49,12 +51,15 @@ class Codeberg extends AppSource { } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { return gh.searchCommon( - query, - 'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100', - 'data', - querySettings: querySettings); + query, + 'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100', + 'data', + querySettings: querySettings, + ); } } diff --git a/lib/app_sources/coolapk.dart b/lib/app_sources/coolapk.dart index 741ec26..4d2bf2c 100644 --- a/lib/app_sources/coolapk.dart +++ b/lib/app_sources/coolapk.dart @@ -19,8 +19,9 @@ class CoolApk extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - r'^https?://(www\.)?coolapk\.com/apk/[^/]+', - caseSensitive: false); + r'^https?://(www\.)?coolapk\.com/apk/[^/]+', + caseSensitive: false, + ); var match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -30,17 +31,19 @@ class CoolApk extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { String appId = Uri.parse(standardUrl).pathSegments.last; return appId; } @override Future getLatestAPKDetails( - String standardUrl, - Map additionalSettings, - ) async { + String standardUrl, + Map additionalSettings, + ) async { String appId = (await tryInferringAppId(standardUrl))!; String apiUrl = 'https://api2.coolapk.com'; @@ -65,13 +68,19 @@ class CoolApk extends AppSource { String changelog = detail['changelog']?.toString() ?? ''; int? releaseDate = detail['lastupdate'] != null ? (detail['lastupdate'] is int - ? detail['lastupdate'] * 1000 - : int.parse(detail['lastupdate'].toString()) * 1000) + ? detail['lastupdate'] * 1000 + : int.parse(detail['lastupdate'].toString()) * 1000) : null; String aid = detail['id'].toString(); // get apk url - String apkUrl = await _getLatestApkUrl(apiUrl, appId, aid, version, headers); + String apkUrl = await _getLatestApkUrl( + apiUrl, + appId, + aid, + version, + headers, + ); if (apkUrl.isEmpty) { throw NoAPKError(); } @@ -89,8 +98,13 @@ class CoolApk extends AppSource { ); } - Future _getLatestApkUrl(String apiUrl, String appId, String aid, - String version, Map? headers) async { + Future _getLatestApkUrl( + String apiUrl, + String appId, + String aid, + String version, + Map? headers, + ) async { String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid'; var res = await sourceRequest(url, {}, followRedirects: false); if (res.statusCode >= 300 && res.statusCode < 400) { @@ -102,13 +116,14 @@ class CoolApk extends AppSource { @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { var tokenPair = _getToken(); // CoolAPK header return { 'User-Agent': - 'Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/12.4.2-2208241-universal', + 'Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/12.4.2-2208241-universal', 'X-App-Id': 'com.coolapk.market', 'X-Requested-With': 'XMLHttpRequest', 'X-Sdk-Int': '30', @@ -128,14 +143,15 @@ class CoolApk extends AppSource { Map _getToken() { final rand = Random(); - String randHexString(int n) => - List.generate(n, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')) - .join() - .toUpperCase(); + String randHexString(int n) => List.generate( + n, + (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join().toUpperCase(); - String randMacAddress() => - List.generate(6, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')) - .join(':'); + String randMacAddress() => List.generate( + 6, + (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'), + ).join(':'); // 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP // device @@ -147,11 +163,13 @@ class CoolApk extends AppSource { const buildNumber = 'SQ1D.220105.007'; // generate deviceCode - String deviceCode = - base64.encode('$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits); + String deviceCode = base64.encode( + '$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits, + ); // generate timestamp - String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); + String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000) + .toString(); String base64TimeStamp = base64.encode(timeStamp.codeUnits); String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString(); String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString(); @@ -164,7 +182,8 @@ class CoolApk extends AppSource { String md5Token = md5.convert(token.codeUnits).toString(); // generate salt and hash - String bcryptSalt = '\$2a\$10\$${base64TimeStamp.substring(0, 14)}/${md5Token.substring(0, 6)}u'; + String bcryptSalt = + '\$2a\$10\$${base64TimeStamp.substring(0, 14)}/${md5Token.substring(0, 6)}u'; String bcryptResult = BCrypt.hashpw(md5Base64Token, bcryptSalt); String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y'); String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}'; diff --git a/lib/app_sources/directAPKLink.dart b/lib/app_sources/directAPKLink.dart index 5fc4d3a..5be1483 100644 --- a/lib/app_sources/directAPKLink.dart +++ b/lib/app_sources/directAPKLink.dart @@ -11,20 +11,23 @@ class DirectAPKLink extends AppSource { name = tr('directAPKLink'); additionalSourceAppSpecificSettingFormItems = [ ...html.additionalSourceAppSpecificSettingFormItems - .where((element) => element - .where((element) => element.key == 'requestHeader') - .isNotEmpty) + .where( + (element) => element + .where((element) => element.key == 'requestHeader') + .isNotEmpty, + ) .toList(), [ GeneratedFormDropdown( - 'defaultPseudoVersioningMethod', - [ - MapEntry('partialAPKHash', tr('partialAPKHash')), - MapEntry('ETag', 'ETag') - ], - label: tr('defaultPseudoVersioningMethod'), - defaultValue: 'partialAPKHash') - ] + 'defaultPseudoVersioningMethod', + [ + MapEntry('partialAPKHash', tr('partialAPKHash')), + MapEntry('ETag', 'ETag'), + ], + label: tr('defaultPseudoVersioningMethod'), + defaultValue: 'partialAPKHash', + ), + ], ]; excludeCommonSettingKeys = [ 'versionExtractionRegEx', @@ -32,7 +35,7 @@ class DirectAPKLink extends AppSource { 'versionDetection', 'useVersionCodeAsOSVersion', 'apkFilterRegEx', - 'autoApkFilterByArch' + 'autoApkFilterByArch', ]; } @@ -51,10 +54,13 @@ class DirectAPKLink extends AppSource { @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) { - return html.getRequestHeaders(additionalSettings, - forAPKDownload: forAPKDownload); + Map additionalSettings, { + bool forAPKDownload = false, + }) { + return html.getRequestHeaders( + additionalSettings, + forAPKDownload: forAPKDownload, + ); } @override @@ -62,8 +68,9 @@ class DirectAPKLink extends AppSource { String standardUrl, Map additionalSettings, ) async { - var additionalSettingsNew = - getDefaultValuesFromFormItems(html.combinedAppSpecificSettingFormItems); + var additionalSettingsNew = getDefaultValuesFromFormItems( + html.combinedAppSpecificSettingFormItems, + ); for (var s in additionalSettings.keys) { if (additionalSettingsNew.containsKey(s)) { additionalSettingsNew[s] = additionalSettings[s]; diff --git a/lib/app_sources/fdroid.dart b/lib/app_sources/fdroid.dart index 25c7175..6d20a02 100644 --- a/lib/app_sources/fdroid.dart +++ b/lib/app_sources/fdroid.dart @@ -17,22 +17,28 @@ class FDroid extends AppSource { canSearch = true; additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormTextField('filterVersionsByRegEx', - label: tr('filterVersionsByRegEx'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) + GeneratedFormTextField( + 'filterVersionsByRegEx', + label: tr('filterVersionsByRegEx'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), ], [ - GeneratedFormSwitch('trySelectingSuggestedVersionCode', - label: tr('trySelectingSuggestedVersionCode')) + GeneratedFormSwitch( + 'trySelectingSuggestedVersionCode', + label: tr('trySelectingSuggestedVersionCode'), + ), ], [ - GeneratedFormSwitch('autoSelectHighestVersionCode', - label: tr('autoSelectHighestVersionCode')) + GeneratedFormSwitch( + 'autoSelectHighestVersionCode', + label: tr('autoSelectHighestVersionCode'), + ), ], ]; } @@ -40,16 +46,18 @@ class FDroid extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegExB = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegExB.firstMatch(url); if (match != null) { url = 'https://${Uri.parse(match.group(0)!).host}/packages/${Uri.parse(url).pathSegments.where((s) => s.trim().isNotEmpty).last}'; } RegExp standardUrlRegExA = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+', + caseSensitive: false, + ); match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -58,8 +66,10 @@ class FDroid extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return Uri.parse(standardUrl).pathSegments.last; } @@ -71,22 +81,28 @@ class FDroid extends AppSource { String? appId = await tryInferringAppId(standardUrl); String host = Uri.parse(standardUrl).host; var details = getAPKUrlsFromFDroidPackagesAPIResponse( - await sourceRequest( - 'https://$host/api/v1/packages/$appId', additionalSettings), - 'https://$host/repo/$appId', - standardUrl, - name, - additionalSettings: additionalSettings); + await sourceRequest( + 'https://$host/api/v1/packages/$appId', + additionalSettings, + ), + 'https://$host/repo/$appId', + standardUrl, + name, + additionalSettings: additionalSettings, + ); if (!hostChanged) { try { var res = await sourceRequest( - 'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml', - additionalSettings); + 'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml', + additionalSettings, + ); var lines = res.body.split('\n'); var authorLines = lines.where((l) => l.startsWith('AuthorName: ')); if (authorLines.isNotEmpty) { - details.names.author = - authorLines.first.split(': ').sublist(1).join(': '); + details.names.author = authorLines.first + .split(': ') + .sublist(1) + .join(': '); } var changelogUrls = lines .where((l) => l.startsWith('Changelog: ')) @@ -110,9 +126,9 @@ class FDroid extends AppSource { if ((isGitHub || isGitLab) && (details.changeLog?.indexOf('/blob/') ?? -1) >= 0) { details.changeLog = (await sourceRequest( - details.changeLog!.replaceFirst('/blob/', '/raw/'), - additionalSettings)) - .body; + details.changeLog!.replaceFirst('/blob/', '/raw/'), + additionalSettings, + )).body; } } } catch (e) { @@ -126,10 +142,14 @@ class FDroid extends AppSource { } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { Response res = await sourceRequest( - 'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {}); + 'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', + {}, + ); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; parse(res.body).querySelectorAll('.package-header').forEach((e) { @@ -145,7 +165,7 @@ class FDroid extends AppSource { urlsWithDescriptions[url] = [ e.querySelector('.package-name')?.text.trim() ?? '', e.querySelector('.package-summary')?.text.trim() ?? - tr('noDescription') + tr('noDescription'), ]; } }); @@ -156,29 +176,36 @@ class FDroid extends AppSource { } APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( - Response res, String apkUrlPrefix, String standardUrl, String sourceName, - {Map additionalSettings = const {}}) { + Response res, + String apkUrlPrefix, + String standardUrl, + String sourceName, { + Map additionalSettings = const {}, + }) { var autoSelectHighestVersionCode = additionalSettings['autoSelectHighestVersionCode'] == true; var trySelectingSuggestedVersionCode = additionalSettings['trySelectingSuggestedVersionCode'] == true; var filterVersionsByRegEx = (additionalSettings['filterVersionsByRegEx'] as String?)?.isNotEmpty == - true - ? additionalSettings['filterVersionsByRegEx'] - : null; + true + ? additionalSettings['filterVersionsByRegEx'] + : null; var apkFilterRegEx = (additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true - ? additionalSettings['apkFilterRegEx'] - : null; + ? additionalSettings['apkFilterRegEx'] + : null; if (res.statusCode == 200) { var response = jsonDecode(res.body); List releases = response['packages'] ?? []; if (apkFilterRegEx != null) { releases = releases.where((rel) { String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk'; - return filterApks([MapEntry(apk, apk)], apkFilterRegEx, false) - .isNotEmpty; + return filterApks( + [MapEntry(apk, apk)], + apkFilterRegEx, + false, + ).isNotEmpty; }).toList(); } if (releases.isEmpty) { @@ -191,8 +218,10 @@ class FDroid extends AppSource { if (trySelectingSuggestedVersionCode && response['suggestedVersionCode'] != null && filterVersionsByRegEx == null) { - var suggestedReleases = releases.where((element) => - element['versionCode'] == response['suggestedVersionCode']); + var suggestedReleases = releases.where( + (element) => + element['versionCode'] == response['suggestedVersionCode'], + ); if (suggestedReleases.isNotEmpty) { releaseChoices = suggestedReleases; version = suggestedReleases.first['versionName']; @@ -203,8 +232,9 @@ class FDroid extends AppSource { version = null; releaseChoices = []; for (var i = 0; i < releases.length; i++) { - if (RegExp(filterVersionsByRegEx!) - .hasMatch(releases[i]['versionName'])) { + if (RegExp( + filterVersionsByRegEx!, + ).hasMatch(releases[i]['versionName'])) { version = releases[i]['versionName']; } } @@ -219,8 +249,9 @@ class FDroid extends AppSource { } // If a suggested release was not already picked, pick all those with the selected version if (releaseChoices.isEmpty) { - releaseChoices = - releases.where((element) => element['versionName'] == version); + releaseChoices = releases.where( + (element) => element['versionName'] == version, + ); } // For the remaining releases, use the toggles to auto-select one if possible if (releaseChoices.length > 1) { @@ -228,8 +259,10 @@ class FDroid extends AppSource { releaseChoices = [releaseChoices.first]; } else if (trySelectingSuggestedVersionCode && response['suggestedVersionCode'] != null) { - var suggestedReleases = releaseChoices.where((element) => - element['versionCode'] == response['suggestedVersionCode']); + var suggestedReleases = releaseChoices.where( + (element) => + element['versionCode'] == response['suggestedVersionCode'], + ); if (suggestedReleases.isNotEmpty) { releaseChoices = suggestedReleases; } @@ -241,8 +274,11 @@ class FDroid extends AppSource { List apkUrls = releaseChoices .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') .toList(); - return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()), - AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last)); + return APKDetails( + version, + getApkUrlsFromUrls(apkUrls.toSet().toList()), + AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last), + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index c8cc536..7d452aa 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -15,15 +15,20 @@ class FDroidRepo extends AppSource { additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormTextField('appIdOrName', - label: tr('appIdOrName'), - hint: tr('reposHaveMultipleApps'), - required: true) + GeneratedFormTextField( + 'appIdOrName', + label: tr('appIdOrName'), + hint: tr('reposHaveMultipleApps'), + required: true, + ), ], [ - GeneratedFormSwitch('pickHighestVersionCode', - label: tr('pickHighestVersionCode'), defaultValue: false) - ] + GeneratedFormSwitch( + 'pickHighestVersionCode', + label: tr('pickHighestVersionCode'), + defaultValue: false, + ), + ], ]; } @@ -54,8 +59,10 @@ class FDroidRepo extends AppSource { } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { String? url = querySettings['url']; if (url == null) { throw NoReleasesError(); @@ -73,11 +80,8 @@ class FDroidRepo extends AppSource { appId.contains(query) || appName.contains(query) || appDesc.contains(query)) { - results[ - '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [ - appName, - appDesc - ]; + results['${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = + [appName, appDesc]; } }); return results; @@ -90,21 +94,21 @@ class FDroidRepo extends AppSource { void runOnAddAppInputChange(String userInput) { additionalSourceAppSpecificSettingFormItems = additionalSourceAppSpecificSettingFormItems.map((row) { - row = row.map((item) { - if (item.key == 'appIdOrName') { - try { - var appId = Uri.parse(userInput).queryParameters['appId']; - if (appId != null && item is GeneratedFormTextField) { - item.required = false; + row = row.map((item) { + if (item.key == 'appIdOrName') { + try { + var appId = Uri.parse(userInput).queryParameters['appId']; + if (appId != null && item is GeneratedFormTextField) { + item.required = false; + } + } catch (e) { + // + } } - } catch (e) { - // - } - } - return item; - }).toList(); - return row; - }).toList(); + return item; + }).toList(); + return row; + }).toList(); } @override @@ -119,8 +123,11 @@ class FDroidRepo extends AppSource { if (appId != null) { app.url = uri .replace( - queryParameters: Map.fromEntries( - [...uri.queryParameters.entries, MapEntry('appId', appId)])) + queryParameters: Map.fromEntries([ + ...uri.queryParameters.entries, + MapEntry('appId', appId), + ]), + ) .toString(); app.additionalSettings['appIdOrName'] = appId; app.id = appId; @@ -133,8 +140,9 @@ class FDroidRepo extends AppSource { Map additionalSettings, ) async { var res = await sourceRequest( - '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}', - additionalSettings); + '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}', + additionalSettings, + ); if (res.statusCode != 200) { var base = url.endsWith('/index.xml') ? url.split('/').reversed.toList().sublist(1).reversed.join('/') @@ -142,7 +150,9 @@ class FDroidRepo extends AppSource { res = await sourceRequest('$base/repo/index.xml', additionalSettings); if (res.statusCode != 200) { res = await sourceRequest( - '$base/fdroid/repo/index.xml', additionalSettings); + '$base/fdroid/repo/index.xml', + additionalSettings, + ); } } return res; @@ -164,8 +174,10 @@ class FDroidRepo extends AppSource { throw NoReleasesError(); } additionalSettings['appIdOrName'] = appIdOrName; - var res = - await sourceRequestWithURLVariants(standardUrl, additionalSettings); + var res = await sourceRequestWithURLVariants( + standardUrl, + additionalSettings, + ); if (res.statusCode == 200) { var body = parse(res.body); var foundApps = body.querySelectorAll('application').where((element) { @@ -202,24 +214,32 @@ class FDroidRepo extends AppSource { throw NoVersionError(); } var latestVersionReleases = releases - .where((element) => - element.querySelector('version')?.innerHtml == latestVersion && - element.querySelector('apkname') != null) + .where( + (element) => + element.querySelector('version')?.innerHtml == latestVersion && + element.querySelector('apkname') != null, + ) .toList(); if (latestVersionReleases.length > 1 && pickHighestVersionCode) { latestVersionReleases.sort((e1, e2) { - return int.parse(e2.querySelector('versioncode')!.innerHtml) - .compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml)); + return int.parse( + e2.querySelector('versioncode')!.innerHtml, + ).compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml)); }); latestVersionReleases = [latestVersionReleases[0]]; } List apkUrls = latestVersionReleases - .map((e) => - '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}') + .map( + (e) => + '${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}', + ) .toList(); - return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), - AppNames(authorName, appName), - releaseDate: releaseDate); + return APKDetails( + latestVersion, + getApkUrlsFromUrls(apkUrls), + AppNames(authorName, appName), + releaseDate: releaseDate, + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index f2490cc..667e93e 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -19,145 +19,185 @@ class GitHub extends AppSource { showReleaseDateAsVersionToggle = true; sourceConfigSettingFormItems = [ - GeneratedFormTextField('github-creds', - label: tr('githubPATLabel'), - password: true, - required: false, - belowWidgets: [ - const SizedBox( - height: 4, + GeneratedFormTextField( + 'github-creds', + label: tr('githubPATLabel'), + password: true, + required: false, + belowWidgets: [ + const SizedBox(height: 4), + GestureDetector( + onTap: () { + launchUrlString( + 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + tr('about'), + style: const TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), ), - GestureDetector( - onTap: () { - launchUrlString( - 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token', - mode: LaunchMode.externalApplication); - }, - child: Text( - tr('about'), - style: const TextStyle( - decoration: TextDecoration.underline, fontSize: 12), - )), - const SizedBox( - height: 4, - ), - ]) + ), + const SizedBox(height: 4), + ], + ), ]; additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSwitch('includePrereleases', - label: tr('includePrereleases'), defaultValue: false) + GeneratedFormSwitch( + 'includePrereleases', + label: tr('includePrereleases'), + defaultValue: false, + ), ], [ - GeneratedFormSwitch('fallbackToOlderReleases', - label: tr('fallbackToOlderReleases'), defaultValue: true) + GeneratedFormSwitch( + 'fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + defaultValue: true, + ), ], [ - GeneratedFormTextField('filterReleaseTitlesByRegEx', - label: tr('filterReleaseTitlesByRegEx'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) + GeneratedFormTextField( + 'filterReleaseTitlesByRegEx', + label: tr('filterReleaseTitlesByRegEx'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), ], [ - GeneratedFormTextField('filterReleaseNotesByRegEx', - label: tr('filterReleaseNotesByRegEx'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) + GeneratedFormTextField( + 'filterReleaseNotesByRegEx', + label: tr('filterReleaseNotesByRegEx'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), ], [GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))], [ GeneratedFormDropdown( - 'sortMethodChoice', - [ - MapEntry('date', tr('releaseDate')), - MapEntry('smartname', tr('smartname')), - MapEntry('none', tr('none')), - MapEntry('smartname-datefallback', - '${tr('smartname')} x ${tr('releaseDate')}'), - MapEntry('name', tr('name')), - ], - label: tr('sortMethod'), - defaultValue: 'date') + 'sortMethodChoice', + [ + MapEntry('date', tr('releaseDate')), + MapEntry('smartname', tr('smartname')), + MapEntry('none', tr('none')), + MapEntry( + 'smartname-datefallback', + '${tr('smartname')} x ${tr('releaseDate')}', + ), + MapEntry('name', tr('name')), + ], + label: tr('sortMethod'), + defaultValue: 'date', + ), ], [ - GeneratedFormSwitch('useLatestAssetDateAsReleaseDate', - label: tr('useLatestAssetDateAsReleaseDate'), defaultValue: false) + GeneratedFormSwitch( + 'useLatestAssetDateAsReleaseDate', + label: tr('useLatestAssetDateAsReleaseDate'), + defaultValue: false, + ), ], [ - GeneratedFormSwitch('releaseTitleAsVersion', - label: tr('releaseTitleAsVersion'), defaultValue: false) - ] + GeneratedFormSwitch( + 'releaseTitleAsVersion', + label: tr('releaseTitleAsVersion'), + defaultValue: false, + ), + ], ]; canSearch = true; searchQuerySettingFormItems = [ - GeneratedFormTextField('minStarCount', - label: tr('minStarCount'), - defaultValue: '0', - additionalValidators: [ - (value) { - try { - int.parse(value ?? '0'); - } catch (e) { - return tr('invalidInput'); - } - return null; + GeneratedFormTextField( + 'minStarCount', + label: tr('minStarCount'), + defaultValue: '0', + additionalValidators: [ + (value) { + try { + int.parse(value ?? '0'); + } catch (e) { + return tr('invalidInput'); } - ]) + return null; + }, + ], + ), ]; } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { const possibleBuildGradleLocations = [ '/app/build.gradle', 'android/app/build.gradle', - 'src/app/build.gradle' + 'src/app/build.gradle', ]; for (var path in possibleBuildGradleLocations) { try { var res = await sourceRequest( - '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path', - additionalSettings); + '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path', + additionalSettings, + ); if (res.statusCode == 200) { try { var body = jsonDecode(res.body); var trimmedLines = utf8 - .decode(base64 - .decode(body['content'].toString().split('\n').join(''))) + .decode( + base64.decode( + body['content'].toString().split('\n').join(''), + ), + ) .split('\n') .map((e) => e.trim()); - var appIds = trimmedLines.where((l) => - l.startsWith('applicationId "') || - l.startsWith('applicationId \'')); - appIds = appIds.map((appId) => appId - .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]); - appIds = appIds.map((appId) { - if (appId.startsWith('\${') && appId.endsWith('}')) { - appId = trimmedLines - .where((l) => l.startsWith( - 'def ${appId.substring(2, appId.length - 1)}')) - .first; - appId = appId.split(appId.contains('"') ? '"' : '\'')[1]; - } - return appId; - }).where((appId) => appId.isNotEmpty); + var appIds = trimmedLines.where( + (l) => + l.startsWith('applicationId "') || + l.startsWith('applicationId \''), + ); + appIds = appIds.map( + (appId) => appId.split( + appId.startsWith('applicationId "') ? '"' : '\'', + )[1], + ); + appIds = appIds + .map((appId) { + if (appId.startsWith('\${') && appId.endsWith('}')) { + appId = trimmedLines + .where( + (l) => l.startsWith( + 'def ${appId.substring(2, appId.length - 1)}', + ), + ) + .first; + appId = appId.split(appId.contains('"') ? '"' : '\'')[1]; + } + return appId; + }) + .where((appId) => appId.isNotEmpty); if (appIds.length == 1) { return appIds.first; } } catch (err) { LogsProvider().add( - 'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}'); + 'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}', + ); } } } catch (err) { @@ -170,8 +210,9 @@ class GitHub extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -181,8 +222,9 @@ class GitHub extends AppSource { @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { var token = await getTokenIfAny(additionalSettings); var headers = {}; if (token != null && token.isNotEmpty) { @@ -201,14 +243,17 @@ class GitHub extends AppSource { Future getTokenIfAny(Map additionalSettings) async { SettingsProvider settingsProvider = SettingsProvider(); await settingsProvider.initializeSettings(); - var sourceConfig = - await getSourceConfigValues(additionalSettings, settingsProvider); + var sourceConfig = await getSourceConfigValues( + additionalSettings, + settingsProvider, + ); String? creds = sourceConfig['github-creds']; if (creds != null) { var userNameEndIndex = creds.indexOf(':'); if (userNameEndIndex > 0) { creds = creds.substring( - userNameEndIndex + 1); // For old username-included token inputs + userNameEndIndex + 1, + ); // For old username-included token inputs } return creds; } else { @@ -228,31 +273,36 @@ class GitHub extends AppSource { 'https://api.${hosts[0]}'; Future convertStandardUrlToAPIUrl( - String standardUrl, Map additionalSettings) async => + String standardUrl, + Map additionalSettings, + ) async => '${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}'; @override String? changeLogPageFromStandardUrl(String standardUrl) => '$standardUrl/releases'; - Future getLatestAPKDetailsCommon(String requestUrl, - String standardUrl, Map additionalSettings, - {Function(Response)? onHttpErrorCode}) async { + Future getLatestAPKDetailsCommon( + String requestUrl, + String standardUrl, + Map additionalSettings, { + Function(Response)? onHttpErrorCode, + }) async { bool includePrereleases = additionalSettings['includePrereleases'] == true; bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; String? regexFilter = (additionalSettings['filterReleaseTitlesByRegEx'] as String?) - ?.isNotEmpty == - true - ? additionalSettings['filterReleaseTitlesByRegEx'] - : null; + ?.isNotEmpty == + true + ? additionalSettings['filterReleaseTitlesByRegEx'] + : null; String? regexNotesFilter = (additionalSettings['filterReleaseNotesByRegEx'] as String?) - ?.isNotEmpty == - true - ? additionalSettings['filterReleaseNotesByRegEx'] - : null; + ?.isNotEmpty == + true + ? additionalSettings['filterReleaseNotesByRegEx'] + : null; bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true; bool useLatestAssetDateAsReleaseDate = additionalSettings['useLatestAssetDateAsReleaseDate'] == true; @@ -262,8 +312,9 @@ class GitHub extends AppSource { if (verifyLatestTag) { var temp = requestUrl.split('?'); Response res = await sourceRequest( - '${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}', - additionalSettings); + '${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}', + additionalSettings, + ); if (res.statusCode != 200) { if (onHttpErrorCode != null) { onHttpErrorCode(res); @@ -278,8 +329,10 @@ class GitHub extends AppSource { if (latestRelease != null) { var latestTag = latestRelease['tag_name'] ?? latestRelease['name']; if (releases - .where((element) => - (element['tag_name'] ?? element['name']) == latestTag) + .where( + (element) => + (element['tag_name'] ?? element['name']) == latestTag, + ) .isEmpty) { releases = [latestRelease, ...releases]; } @@ -299,10 +352,10 @@ class GitHub extends AppSource { DateTime? getPublishDateFromRelease(dynamic rel) => rel?['published_at'] != null - ? DateTime.parse(rel['published_at']) - : rel?['commit']?['created'] != null - ? DateTime.parse(rel['commit']['created']) - : null; + ? DateTime.parse(rel['published_at']) + : rel?['commit']?['created'] != null + ? DateTime.parse(rel['commit']['created']) + : null; DateTime? getNewestAssetDateFromRelease(dynamic rel) { var allAssets = rel['assets'] as List?; var filteredAssets = rel['filteredAssets'] as List?; @@ -323,8 +376,8 @@ class GitHub extends AppSource { DateTime? getReleaseDateFromRelease(dynamic rel, bool useAssetDate) => !useAssetDate - ? getPublishDateFromRelease(rel) - : getNewestAssetDateFromRelease(rel); + ? getPublishDateFromRelease(rel) + : getNewestAssetDateFromRelease(rel); if (sortMethod == 'none') { releases = releases.reversed.toList(); @@ -340,29 +393,40 @@ class GitHub extends AppSource { } else { var nameA = a['tag_name'] ?? a['name']; var nameB = b['tag_name'] ?? b['name']; - var stdFormats = findStandardFormatsForVersion(nameA, false) - .intersection(findStandardFormatsForVersion(nameB, false)); + var stdFormats = findStandardFormatsForVersion( + nameA, + false, + ).intersection(findStandardFormatsForVersion(nameB, false)); if (sortMethod == 'date' || (sortMethod == 'smartname-datefallback' && stdFormats.isEmpty)) { return (getReleaseDateFromRelease( - a, useLatestAssetDateAsReleaseDate) ?? + a, + useLatestAssetDateAsReleaseDate, + ) ?? DateTime(1)) - .compareTo(getReleaseDateFromRelease( - b, useLatestAssetDateAsReleaseDate) ?? - DateTime(0)); + .compareTo( + getReleaseDateFromRelease( + b, + useLatestAssetDateAsReleaseDate, + ) ?? + DateTime(0), + ); } else { if (sortMethod != 'name' && stdFormats.isNotEmpty) { var reg = RegExp(stdFormats.last); var matchA = reg.firstMatch(nameA); var matchB = reg.firstMatch(nameB); return compareAlphaNumeric( - (nameA as String).substring(matchA!.start, matchA.end), - (nameB as String).substring(matchB!.start, matchB.end)); + (nameA as String).substring(matchA!.start, matchA.end), + (nameB as String).substring(matchB!.start, matchB.end), + ); } else { // 'name' return compareAlphaNumeric( - (nameA as String), (nameB as String)); + (nameA as String), + (nameB as String), + ); } } } @@ -374,9 +438,11 @@ class GitHub extends AppSource { latestRelease != (releases[releases.length - 1]['tag_name'] ?? releases[0]['name'])) { - var ind = releases.indexWhere((element) => - (latestRelease['tag_name'] ?? latestRelease['name']) == - (element['tag_name'] ?? element['name'])); + var ind = releases.indexWhere( + (element) => + (latestRelease['tag_name'] ?? latestRelease['name']) == + (element['tag_name'] ?? element['name']), + ); if (ind >= 0) { releases.add(releases.removeAt(ind)); } @@ -404,8 +470,9 @@ class GitHub extends AppSource { continue; } if (regexNotesFilter != null && - !RegExp(regexNotesFilter) - .hasMatch(((releases[i]['body'] as String?) ?? '').trim())) { + !RegExp( + regexNotesFilter, + ).hasMatch(((releases[i]['body'] as String?) ?? '').trim())) { continue; } var allAssetsWithUrls = findReleaseAssetUrls(releases[i]); @@ -413,24 +480,31 @@ class GitHub extends AppSource { .map((e) => e['final_url'] as MapEntry) .toList(); var apkAssetsWithUrls = allAssetsWithUrls - .where((element) => - (element['final_url'] as MapEntry) - .key - .toLowerCase() - .endsWith('.apk')) + .where( + (element) => (element['final_url'] as MapEntry) + .key + .toLowerCase() + .endsWith('.apk'), + ) .toList(); var filteredApkUrls = filterApks( - apkAssetsWithUrls - .map((e) => e['final_url'] as MapEntry) - .toList(), - additionalSettings['apkFilterRegEx'], - additionalSettings['invertAPKFilter']); + apkAssetsWithUrls + .map((e) => e['final_url'] as MapEntry) + .toList(), + additionalSettings['apkFilterRegEx'], + additionalSettings['invertAPKFilter'], + ); var filteredApks = apkAssetsWithUrls - .where((e) => filteredApkUrls - .where((e2) => - e2.key == (e['final_url'] as MapEntry).key) - .isNotEmpty) + .where( + (e) => filteredApkUrls + .where( + (e2) => + e2.key == + (e['final_url'] as MapEntry).key, + ) + .isNotEmpty, + ) .toList(); if (filteredApks.isEmpty && additionalSettings['trackOnly'] != true) { @@ -441,17 +515,23 @@ class GitHub extends AppSource { targetRelease['filteredAssets'] = filteredApks; targetRelease['version'] = additionalSettings['releaseTitleAsVersion'] == true - ? nameToFilter - : targetRelease['tag_name'] ?? targetRelease['name']; + ? nameToFilter + : targetRelease['tag_name'] ?? targetRelease['name']; if (targetRelease['tarball_url'] != null) { - allAssetUrls.add(MapEntry( + allAssetUrls.add( + MapEntry( (targetRelease['version'] ?? 'source') + '.tar.gz', - targetRelease['tarball_url'])); + targetRelease['tarball_url'], + ), + ); } if (targetRelease['zipball_url'] != null) { - allAssetUrls.add(MapEntry( + allAssetUrls.add( + MapEntry( (targetRelease['version'] ?? 'source') + '.zip', - targetRelease['zipball_url'])); + targetRelease['zipball_url'], + ), + ); } targetRelease['allAssetUrls'] = allAssetUrls; break; @@ -462,19 +542,22 @@ class GitHub extends AppSource { String? version = targetRelease['version']; DateTime? releaseDate = getReleaseDateFromRelease( - targetRelease, useLatestAssetDateAsReleaseDate); + targetRelease, + useLatestAssetDateAsReleaseDate, + ); if (version == null) { throw NoVersionError(); } var changeLog = (targetRelease['body'] ?? '').toString(); return APKDetails( - version, - targetRelease['apkUrls'] as List>, - getAppNames(standardUrl), - releaseDate: releaseDate, - changeLog: changeLog.isEmpty ? null : changeLog, - allAssetUrls: - targetRelease['allAssetUrls'] as List>); + version, + targetRelease['apkUrls'] as List>, + getAppNames(standardUrl), + releaseDate: releaseDate, + changeLog: changeLog.isEmpty ? null : changeLog, + allAssetUrls: + targetRelease['allAssetUrls'] as List>, + ); } else { if (onHttpErrorCode != null) { onHttpErrorCode(res); @@ -484,19 +567,26 @@ class GitHub extends AppSource { } getLatestAPKDetailsCommon2( - String standardUrl, - Map additionalSettings, - Future Function(bool) reqUrlGenerator, - dynamic Function(Response)? onHttpErrorCode) async { + String standardUrl, + Map additionalSettings, + Future Function(bool) reqUrlGenerator, + dynamic Function(Response)? onHttpErrorCode, + ) async { try { return await getLatestAPKDetailsCommon( - await reqUrlGenerator(false), standardUrl, additionalSettings, - onHttpErrorCode: onHttpErrorCode); + await reqUrlGenerator(false), + standardUrl, + additionalSettings, + onHttpErrorCode: onHttpErrorCode, + ); } catch (err) { if (err is NoReleasesError && additionalSettings['trackOnly'] == true) { return await getLatestAPKDetailsCommon( - await reqUrlGenerator(true), standardUrl, additionalSettings, - onHttpErrorCode: onHttpErrorCode); + await reqUrlGenerator(true), + standardUrl, + additionalSettings, + onHttpErrorCode: onHttpErrorCode, + ); } else { rethrow; } @@ -508,12 +598,16 @@ class GitHub extends AppSource { String standardUrl, Map additionalSettings, ) async { - return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings, - (bool useTagUrl) async { - return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100'; - }, (Response res) { - rateLimitErrorCheck(res); - }); + return await getLatestAPKDetailsCommon2( + standardUrl, + additionalSettings, + (bool useTagUrl) async { + return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100'; + }, + (Response res) { + rateLimitErrorCheck(res); + }, + ); } AppNames getAppNames(String standardUrl) { @@ -523,9 +617,12 @@ class GitHub extends AppSource { } Future>> searchCommon( - String query, String requestUrl, String rootProp, - {Function(Response)? onHttpErrorCode, - Map querySettings = const {}}) async { + String query, + String requestUrl, + String rootProp, { + Function(Response)? onHttpErrorCode, + Map querySettings = const {}, + }) async { Response res = await sourceRequest(requestUrl, {}); if (res.statusCode == 200) { int minStarCount = querySettings['minStarCount'] != null @@ -540,8 +637,8 @@ class GitHub extends AppSource { ((e['archived'] == true ? '[ARCHIVED] ' : '') + (e['description'] != null ? e['description'] as String - : tr('noDescription'))) - ] + : tr('noDescription'))), + ], }); } } @@ -555,22 +652,27 @@ class GitHub extends AppSource { } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { return searchCommon( - query, - '${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100', - 'items', onHttpErrorCode: (Response res) { - rateLimitErrorCheck(res); - }, querySettings: querySettings); + query, + '${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100', + 'items', + onHttpErrorCode: (Response res) { + rateLimitErrorCheck(res); + }, + querySettings: querySettings, + ); } rateLimitErrorCheck(Response res) { if (res.headers['x-ratelimit-remaining'] == '0') { throw RateLimitError( - (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / - 60000000) - .round()); + (int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000) + .round(), + ); } } } diff --git a/lib/app_sources/gitlab.dart b/lib/app_sources/gitlab.dart index 4c0cd2f..fab4421 100644 --- a/lib/app_sources/gitlab.dart +++ b/lib/app_sources/gitlab.dart @@ -18,36 +18,41 @@ class GitLab extends AppSource { showReleaseDateAsVersionToggle = true; sourceConfigSettingFormItems = [ - GeneratedFormTextField('gitlab-creds', - label: tr('gitlabPATLabel'), - password: true, - required: false, - belowWidgets: [ - const SizedBox( - height: 4, + GeneratedFormTextField( + 'gitlab-creds', + label: tr('gitlabPATLabel'), + password: true, + required: false, + belowWidgets: [ + const SizedBox(height: 4), + GestureDetector( + onTap: () { + launchUrlString( + 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token', + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + tr('about'), + style: const TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), ), - GestureDetector( - onTap: () { - launchUrlString( - 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token', - mode: LaunchMode.externalApplication); - }, - child: Text( - tr('about'), - style: const TextStyle( - decoration: TextDecoration.underline, fontSize: 12), - )), - const SizedBox( - height: 4, - ) - ]) + ), + const SizedBox(height: 4), + ], + ), ]; additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSwitch('fallbackToOlderReleases', - label: tr('fallbackToOlderReleases'), defaultValue: true) - ] + GeneratedFormSwitch( + 'fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + defaultValue: true, + ), + ], ]; } @@ -55,11 +60,13 @@ class GitLab extends AppSource { String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { var urlSegments = url.split('/'); var cutOffIndex = urlSegments.indexWhere((s) => s == '-'); - url = - urlSegments.sublist(0, cutOffIndex <= 0 ? null : cutOffIndex).join('/'); + url = urlSegments + .sublist(0, cutOffIndex <= 0 ? null : cutOffIndex) + .join('/'); RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -70,15 +77,19 @@ class GitLab extends AppSource { Future getPATIfAny(Map additionalSettings) async { SettingsProvider settingsProvider = SettingsProvider(); await settingsProvider.initializeSettings(); - var sourceConfig = - await getSourceConfigValues(additionalSettings, settingsProvider); + var sourceConfig = await getSourceConfigValues( + additionalSettings, + settingsProvider, + ); String? creds = sourceConfig['gitlab-creds']; return creds != null && creds.isNotEmpty ? creds : null; } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { var url = 'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}'; var res = await sourceRequest(url, {}); @@ -90,7 +101,7 @@ class GitLab extends AppSource { for (var element in json) { results['https://${hosts[0]}/${element['path_with_namespace']}'] = [ element['name_with_namespace'], - element['description'] ?? tr('noDescription') + element['description'] ?? tr('noDescription'), ]; } return results; @@ -102,8 +113,9 @@ class GitLab extends AppSource { @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { // Change headers to pacify, e.g. cloudflare protection // Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697) var headers = {}; @@ -116,8 +128,11 @@ class GitLab extends AppSource { } @override - Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, - Map additionalSettings) async { + Future apkUrlPrefetchModifier( + String apkUrl, + String standardUrl, + Map additionalSettings, + ) async { String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {}); String optionalAuth = (PAT != null) ? 'private_token=$PAT' : ''; return '$apkUrl${(Uri.parse(apkUrl).query.isEmpty ? '?' : '&')}$optionalAuth'; @@ -139,8 +154,9 @@ class GitLab extends AppSource { // Get project ID Response res0 = await sourceRequest( - 'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth', - additionalSettings); + 'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth', + additionalSettings, + ); if (res0.statusCode != 200) { throw getObtainiumHttpError(res0); } @@ -151,8 +167,9 @@ class GitLab extends AppSource { // Request data from REST API Response res = await sourceRequest( - 'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth', - additionalSettings); + 'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth', + additionalSettings, + ); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } @@ -166,11 +183,13 @@ class GitLab extends AppSource { var url = (e['direct_asset_url'] ?? e['url'] ?? '') as String; var parsedUrl = url.isNotEmpty ? Uri.parse(url) : null; return MapEntry( - (e['name'] ?? - (parsedUrl != null && parsedUrl.pathSegments.isNotEmpty - ? parsedUrl.pathSegments.last - : 'unknown')) as String, - (e['direct_asset_url'] ?? e['url'] ?? '') as String); + (e['name'] ?? + (parsedUrl != null && parsedUrl.pathSegments.isNotEmpty + ? parsedUrl.pathSegments.last + : 'unknown')) + as String, + (e['direct_asset_url'] ?? e['url'] ?? '') as String, + ); }) .where((s) => s.key.isNotEmpty) .toList(); @@ -193,11 +212,15 @@ class GitLab extends AppSource { } var releaseDateString = e['released_at'] ?? e['created_at'] ?? e['commit']?['created_at']; - DateTime? releaseDate = - releaseDateString != null ? DateTime.parse(releaseDateString) : null; - return APKDetails(e['tag_name'] ?? e['name'], apkUrls.entries.toList(), - AppNames(names.author, names.name.split('/').last), - releaseDate: releaseDate); + DateTime? releaseDate = releaseDateString != null + ? DateTime.parse(releaseDateString) + : null; + return APKDetails( + e['tag_name'] ?? e['name'], + apkUrls.entries.toList(), + AppNames(names.author, names.name.split('/').last), + releaseDate: releaseDate, + ); }); if (apkDetailsList.isEmpty) { throw NoReleasesError(); @@ -208,8 +231,9 @@ class GitLab extends AppSource { bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) { - apkDetailsList = - apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList(); + apkDetailsList = apkDetailsList + .where((e) => e.apkUrls.isNotEmpty) + .toList(); finalResult = apkDetailsList.first; } @@ -218,10 +242,13 @@ class GitLab extends AppSource { } finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) { - if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+') - .hasMatch(apkUrl.value)) { + if (RegExp( + '^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+', + ).hasMatch(apkUrl.value)) { return MapEntry( - apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/')); + apkUrl.key, + apkUrl.value.replaceFirst('/file/', '/raw/'), + ); } else { return apkUrl; } diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index 9037f6d..437cb6f 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -100,28 +100,37 @@ bool _isNumeric(String s) { return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57; } -List> getLinksInLines(String lines) => RegExp( - r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?') - .allMatches(lines) - .map((match) => - MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? '')) - .toList(); +List> getLinksInLines(String lines) => + RegExp( + r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?', + ) + .allMatches(lines) + .map( + (match) => + MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''), + ) + .toList(); // Given an HTTP response, grab some links according to the common additional settings // (those that apply to intermediate and final steps) Future>> grabLinksCommon( - Response res, Map additionalSettings) async { + Response res, + Map additionalSettings, +) async { if (res.statusCode != 200) { throw getObtainiumHttpError(res); } var html = parse(res.body); List> allLinks = html .querySelectorAll('a') - .map((element) => MapEntry( + .map( + (element) => MapEntry( element.attributes['href'] ?? '', element.text.isNotEmpty ? element.text - : (element.attributes['href'] ?? '').split('/').last)) + : (element.attributes['href'] ?? '').split('/').last, + ), + ) .where((element) => element.key.isNotEmpty) .map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value)) .toList(); @@ -134,9 +143,13 @@ Future>> grabLinksCommon( var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body)); allLinks = getLinksInLines(jsonStrings.join('\n')); if (allLinks.isEmpty) { - allLinks = getLinksInLines(jsonStrings.map((l) { - return ensureAbsoluteUrl(l, res.request!.url); - }).join('\n')); + allLinks = getLinksInLines( + jsonStrings + .map((l) { + return ensureAbsoluteUrl(l, res.request!.url); + }) + .join('\n'), + ); } } catch (e) { // @@ -165,17 +178,20 @@ Future>> grabLinksCommon( } catch (e) { // Some links may not have valid encoding } - return Uri.parse(filterLinkByText ? element.value : link) - .path - .toLowerCase() - .endsWith('.apk'); + return Uri.parse( + filterLinkByText ? element.value : link, + ).path.toLowerCase().endsWith('.apk'); }).toList(); } if (!skipSort) { - links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true - ? compareAlphaNumeric(a.key.split('/').where((e) => e.isNotEmpty).last, - b.key.split('/').where((e) => e.isNotEmpty).last) - : compareAlphaNumeric(a.key, b.key)); + links.sort( + (a, b) => additionalSettings['sortByLastLinkSegment'] == true + ? compareAlphaNumeric( + a.key.split('/').where((e) => e.isNotEmpty).last, + b.key.split('/').where((e) => e.isNotEmpty).last, + ) + : compareAlphaNumeric(a.key, b.key), + ); } if (additionalSettings['reverseSort'] == true) { links = links.reversed.toList(); @@ -201,102 +217,119 @@ class HTML extends AppSource { var finalStepFormitems = [ [ - GeneratedFormTextField('customLinkFilterRegex', - label: tr('customLinkFilterRegex'), - hint: 'download/(.*/)?(android|apk|mobile)', - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) + GeneratedFormTextField( + 'customLinkFilterRegex', + label: tr('customLinkFilterRegex'), + hint: 'download/(.*/)?(android|apk|mobile)', + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), ], [ - GeneratedFormSwitch('versionExtractWholePage', - label: tr('versionExtractWholePage')) - ] + GeneratedFormSwitch( + 'versionExtractWholePage', + label: tr('versionExtractWholePage'), + ), + ], ]; var commonFormItems = [ [GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))], [GeneratedFormSwitch('skipSort', label: tr('skipSort'))], [GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))], [ - GeneratedFormSwitch('sortByLastLinkSegment', - label: tr('sortByLastLinkSegment')) + GeneratedFormSwitch( + 'sortByLastLinkSegment', + label: tr('sortByLastLinkSegment'), + ), ], ]; var intermediateFormItems = [ [ - GeneratedFormTextField('customLinkFilterRegex', - label: tr('intermediateLinkRegex'), - hint: '([0-9]+.)*[0-9]+/\$', - required: true, - additionalValidators: [(value) => regExValidator(value)]) + GeneratedFormTextField( + 'customLinkFilterRegex', + label: tr('intermediateLinkRegex'), + hint: '([0-9]+.)*[0-9]+/\$', + required: true, + additionalValidators: [(value) => regExValidator(value)], + ), ], [ - GeneratedFormSwitch('autoLinkFilterByArch', - label: tr('autoLinkFilterByArch'), defaultValue: false) + GeneratedFormSwitch( + 'autoLinkFilterByArch', + label: tr('autoLinkFilterByArch'), + defaultValue: false, + ), ], ]; HTML() { additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSubForm( - 'intermediateLink', [...intermediateFormItems, ...commonFormItems], - label: tr('intermediateLink')) + GeneratedFormSubForm('intermediateLink', [ + ...intermediateFormItems, + ...commonFormItems, + ], label: tr('intermediateLink')), ], finalStepFormitems[0], ...commonFormItems, ...finalStepFormitems.sublist(1), [ GeneratedFormSubForm( - 'requestHeader', + 'requestHeader', + [ [ - [ - GeneratedFormTextField('requestHeader', - label: tr('requestHeader'), - required: false, - additionalValidators: [ - (value) { - if ((value ?? 'empty:valid') - .split(':') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .length < - 2) { - return tr('invalidInput'); - } - return null; - } - ]) - ] + GeneratedFormTextField( + 'requestHeader', + label: tr('requestHeader'), + required: false, + additionalValidators: [ + (value) { + if ((value ?? 'empty:valid') + .split(':') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .length < + 2) { + return tr('invalidInput'); + } + return null; + }, + ], + ), ], - label: tr('requestHeader'), - defaultValue: [ - { - 'requestHeader': - 'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' - } - ]) + ], + label: tr('requestHeader'), + defaultValue: [ + { + 'requestHeader': + 'User-Agent: Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36', + }, + ], + ), ], [ GeneratedFormDropdown( - 'defaultPseudoVersioningMethod', - [ - MapEntry('partialAPKHash', tr('partialAPKHash')), - MapEntry('APKLinkHash', tr('APKLinkHash')), - MapEntry('ETag', 'ETag') - ], - label: tr('defaultPseudoVersioningMethod'), - defaultValue: 'partialAPKHash') - ] + 'defaultPseudoVersioningMethod', + [ + MapEntry('partialAPKHash', tr('partialAPKHash')), + MapEntry('APKLinkHash', tr('APKLinkHash')), + MapEntry('ETag', 'ETag'), + ], + label: tr('defaultPseudoVersioningMethod'), + defaultValue: 'partialAPKHash', + ), + ], ]; } @override Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { if (additionalSettings.isNotEmpty) { if (additionalSettings['requestHeader']?.isNotEmpty != true) { additionalSettings['requestHeader'] = []; @@ -336,8 +369,9 @@ class HTML extends AppSource { .toList(); for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { var intLinks = await grabLinksCommon( - await sourceRequest(currentUrl, additionalSettings), - additionalSettings['intermediateLink'][i]); + await sourceRequest(currentUrl, additionalSettings), + additionalSettings['intermediateLink'][i], + ); if (intLinks.isEmpty) { throw NoReleasesError(note: currentUrl); } else { @@ -353,11 +387,17 @@ class HTML extends AppSource { String versionExtractionWholePageString = currentUrl; if (additionalSettings['directAPKLink'] != true) { Response res = await sourceRequest(currentUrl, additionalSettings); - versionExtractionWholePageString = - res.body.split('\r\n').join('\n').split('\n').join('\\n'); + versionExtractionWholePageString = res.body + .split('\r\n') + .join('\n') + .split('\n') + .join('\\n'); links = await grabLinksCommon(res, additionalSettings); - links = filterApks(links, additionalSettings['apkFilterRegEx'], - additionalSettings['invertAPKFilter']); + links = filterApks( + links, + additionalSettings['apkFilterRegEx'], + additionalSettings['invertAPKFilter'], + ); if (links.isEmpty) { throw NoReleasesError(note: currentUrl); } @@ -373,37 +413,45 @@ class HTML extends AppSource { } String? version; version = extractVersion( - additionalSettings['versionExtractionRegEx'] as String?, - additionalSettings['matchGroupToUse'] as String?, - additionalSettings['versionExtractWholePage'] == true - ? versionExtractionWholePageString - : relDecoded); - var apkReqHeaders = - await getRequestHeaders(additionalSettings, forAPKDownload: true); + additionalSettings['versionExtractionRegEx'] as String?, + additionalSettings['matchGroupToUse'] as String?, + additionalSettings['versionExtractWholePage'] == true + ? versionExtractionWholePageString + : relDecoded, + ); + var apkReqHeaders = await getRequestHeaders( + additionalSettings, + forAPKDownload: true, + ); if (version == null && additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') { - version = await checkETagHeader(rel, - headers: apkReqHeaders, - allowInsecure: additionalSettings['allowInsecure'] == true); + version = await checkETagHeader( + rel, + headers: apkReqHeaders, + allowInsecure: additionalSettings['allowInsecure'] == true, + ); if (version == null) { throw NoVersionError(); } } version ??= additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash' - ? rel.hashCode.toString() - : (await checkPartialDownloadHashDynamic(rel, - headers: apkReqHeaders, - allowInsecure: additionalSettings['allowInsecure'] == true)) - .toString(); + ? rel.hashCode.toString() + : (await checkPartialDownloadHashDynamic( + rel, + headers: apkReqHeaders, + allowInsecure: additionalSettings['allowInsecure'] == true, + )).toString(); return APKDetails( - version, - [rel].map((e) { - var uri = Uri.parse(e); - var fileName = - uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.origin; - return MapEntry('${e.hashCode}-$fileName', e); - }).toList(), - AppNames(uri.host, tr('app'))); + version, + [rel].map((e) { + var uri = Uri.parse(e); + var fileName = uri.pathSegments.isNotEmpty + ? uri.pathSegments.last + : uri.origin; + return MapEntry('${e.hashCode}-$fileName', e); + }).toList(), + AppNames(uri.host, tr('app')), + ); } } diff --git a/lib/app_sources/huaweiappgallery.dart b/lib/app_sources/huaweiappgallery.dart index 0716597..2d09e60 100644 --- a/lib/app_sources/huaweiappgallery.dart +++ b/lib/app_sources/huaweiappgallery.dart @@ -14,8 +14,9 @@ class HuaweiAppGallery extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -27,9 +28,14 @@ class HuaweiAppGallery extends AppSource { 'https://${hosts[0].replaceAll('appgallery.huawei', 'appgallery.cloud.huawei')}/appdl/${standardUrl.split('/').last}'; requestAppdlRedirect( - String dlUrl, Map additionalSettings) async { - Response res = - await sourceRequest(dlUrl, additionalSettings, followRedirects: false); + String dlUrl, + Map additionalSettings, + ) async { + Response res = await sourceRequest( + dlUrl, + additionalSettings, + followRedirects: false, + ); if (res.statusCode == 200 || res.statusCode == 302 || res.statusCode == 304) { @@ -53,8 +59,10 @@ class HuaweiAppGallery extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { String dlUrl = getDlUrl(standardUrl); Response res = await requestAppdlRedirect(dlUrl, additionalSettings); return res.headers['location'] != null @@ -76,8 +84,11 @@ class HuaweiAppGallery extends AppSource { if (appId.isEmpty) { throw NoReleasesError(); } - var relDateStr = - res.headers['location']?.split('?')[0].split('.').reversed.toList()[1]; + var relDateStr = res.headers['location'] + ?.split('?')[0] + .split('.') + .reversed + .toList()[1]; if (relDateStr == null || relDateStr.length != 10) { throw NoVersionError(); } @@ -88,10 +99,15 @@ class HuaweiAppGallery extends AppSource { relDateStrAdj.insert((i + i ~/ 2 - 1), '-'); i += 2; } - var relDate = - DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join('')); + var relDate = DateFormat( + 'yy-MM-dd-HH-mm', + 'en_US', + ).parse(relDateStrAdj.join('')); return APKDetails( - relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId), - releaseDate: relDate); + relDateStr, + [MapEntry('$appId.apk', dlUrl)], + AppNames(name, appId), + releaseDate: relDate, + ); } } diff --git a/lib/app_sources/izzyondroid.dart b/lib/app_sources/izzyondroid.dart index 2960c6e..a6134ad 100644 --- a/lib/app_sources/izzyondroid.dart +++ b/lib/app_sources/izzyondroid.dart @@ -16,13 +16,15 @@ class IzzyOnDroid extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegExA = RegExp( - '^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+', - caseSensitive: false); + '^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegExA.firstMatch(url); if (match == null) { RegExp standardUrlRegExB = RegExp( - '^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+', - caseSensitive: false); + '^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+', + caseSensitive: false, + ); match = standardUrlRegExB.firstMatch(url); } if (match == null) { @@ -32,8 +34,10 @@ class IzzyOnDroid extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return fd.tryInferringAppId(standardUrl); } @@ -44,12 +48,14 @@ class IzzyOnDroid extends AppSource { ) async { String? appId = await tryInferringAppId(standardUrl); return fd.getAPKUrlsFromFDroidPackagesAPIResponse( - await sourceRequest( - 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId', - additionalSettings), - 'https://android.izzysoft.de/frepo/$appId', - standardUrl, - name, - additionalSettings: additionalSettings); + await sourceRequest( + 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId', + additionalSettings, + ), + 'https://android.izzysoft.de/frepo/$appId', + standardUrl, + name, + additionalSettings: additionalSettings, + ); } } diff --git a/lib/app_sources/jenkins.dart b/lib/app_sources/jenkins.dart index 39a4f79..763eed6 100644 --- a/lib/app_sources/jenkins.dart +++ b/lib/app_sources/jenkins.dart @@ -31,14 +31,17 @@ class Jenkins extends AppSource { ) async { standardUrl = trimJobUrl(standardUrl); Response res = await sourceRequest( - '$standardUrl/lastSuccessfulBuild/api/json', additionalSettings); + '$standardUrl/lastSuccessfulBuild/api/json', + additionalSettings, + ); if (res.statusCode == 200) { var json = jsonDecode(res.body); var releaseDate = json['timestamp'] == null ? null : DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int); - var version = - json['number'] == null ? null : (json['number'] as int).toString(); + var version = json['number'] == null + ? null + : (json['number'] as int).toString(); if (version == null) { throw NoVersionError(); } @@ -51,16 +54,21 @@ class Jenkins extends AppSource { return path == null ? const MapEntry('', '') : MapEntry( - (e['fileName'] ?? e['relativePath']) as String, path); + (e['fileName'] ?? e['relativePath']) as String, + path, + ); }) - .where((url) => - url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk')) + .where( + (url) => + url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'), + ) .toList(); return APKDetails( - version, - apkUrls, - releaseDate: releaseDate, - AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last)); + version, + apkUrls, + releaseDate: releaseDate, + AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last), + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index 3980930..9838392 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -12,8 +12,9 @@ class Mullvad extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -31,7 +32,9 @@ class Mullvad extends AppSource { Map additionalSettings, ) async { Response res = await sourceRequest( - '$standardUrl/en/download/android', additionalSettings); + '$standardUrl/en/download/android', + additionalSettings, + ); if (res.statusCode == 200) { var versions = parse(res.body) .querySelectorAll('p') @@ -53,17 +56,18 @@ class Mullvad extends AppSource { String? changeLog; try { changeLog = (await GitHub().getLatestAPKDetails( - 'https://github.com/mullvad/mullvadvpn-app', - {'fallbackToOlderReleases': true})) - .changeLog; + 'https://github.com/mullvad/mullvadvpn-app', + {'fallbackToOlderReleases': true}, + )).changeLog; } catch (e) { // Ignore } return APKDetails( - versions[0], - getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']), - AppNames(name, 'Mullvad-VPN'), - changeLog: changeLog); + versions[0], + getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']), + AppNames(name, 'Mullvad-VPN'), + changeLog: changeLog, + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/neutroncode.dart b/lib/app_sources/neutroncode.dart index cc3d501..8efca3d 100644 --- a/lib/app_sources/neutroncode.dart +++ b/lib/app_sources/neutroncode.dart @@ -12,8 +12,9 @@ class NeutronCode extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -89,24 +90,31 @@ class NeutronCode extends AppSource { if (filename == null) { throw NoReleasesError(); } - var version = - http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml; + var version = http + .querySelector('.pd-version-txt') + ?.nextElementSibling + ?.innerHtml; if (version == null) { throw NoVersionError(); } String? apkUrl = 'https://${hosts[0]}/download/$filename'; - var dateStringOriginal = - http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml; + var dateStringOriginal = http + .querySelector('.pd-date-txt') + ?.nextElementSibling + ?.innerHtml; var dateString = dateStringOriginal != null ? (customDateParse(dateStringOriginal)) : null; var changeLogElements = http.querySelectorAll('.pd-fdesc p'); - return APKDetails(version, getApkUrlsFromUrls([apkUrl]), - AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last), - releaseDate: dateString != null ? DateTime.parse(dateString) : null, - changeLog: changeLogElements.isNotEmpty - ? changeLogElements.last.innerHtml - : null); + return APKDetails( + version, + getApkUrlsFromUrls([apkUrl]), + AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last), + releaseDate: dateString != null ? DateTime.parse(dateString) : null, + changeLog: changeLogElements.isNotEmpty + ? changeLogElements.last.innerHtml + : null, + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/rustore.dart b/lib/app_sources/rustore.dart index 6214dbe..22222d4 100644 --- a/lib/app_sources/rustore.dart +++ b/lib/app_sources/rustore.dart @@ -18,8 +18,9 @@ class RuStore extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -28,16 +29,18 @@ class RuStore extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return Uri.parse(standardUrl).pathSegments.last; } Future decodeString(String str) async { try { return (await CharsetDetector.autoDecode( - Uint8List.fromList(str.codeUnits))) - .string; + Uint8List.fromList(str.codeUnits), + )).string; } catch (e) { return str; } @@ -50,8 +53,9 @@ class RuStore extends AppSource { ) async { String? appId = await tryInferringAppId(standardUrl); Response res0 = await sourceRequest( - 'https://backapi.rustore.ru/applicationData/overallInfo/$appId', - additionalSettings); + 'https://backapi.rustore.ru/applicationData/overallInfo/$appId', + additionalSettings, + ); if (res0.statusCode != 200) { throw getObtainiumHttpError(res0); } @@ -74,10 +78,11 @@ class RuStore extends AppSource { } Response res1 = await sourceRequest( - 'https://backapi.rustore.ru/applicationData/download-link', - additionalSettings, - followRedirects: false, - postBody: {"appId": appDetails['appId'], "firstInstall": true}); + 'https://backapi.rustore.ru/applicationData/download-link', + additionalSettings, + followRedirects: false, + postBody: {"appId": appDetails['appId'], "firstInstall": true}, + ); var downloadDetails = jsonDecode(res1.body)['body']; if (res1.statusCode != 200 || downloadDetails['apkUrl'] == null) { throw NoAPKError(); @@ -88,13 +93,16 @@ class RuStore extends AppSource { changeLog = changeLog != null ? await decodeString(changeLog) : null; return APKDetails( - version, - getApkUrlsFromUrls([ - (downloadDetails['apkUrl'] as String) - .replaceAll(RegExp('\\.zip\$'), '.apk') - ]), - AppNames(author, appName), - releaseDate: relDate, - changeLog: changeLog); + version, + getApkUrlsFromUrls([ + (downloadDetails['apkUrl'] as String).replaceAll( + RegExp('\\.zip\$'), + '.apk', + ), + ]), + AppNames(author, appName), + releaseDate: relDate, + changeLog: changeLog, + ); } } diff --git a/lib/app_sources/sourceforge.dart b/lib/app_sources/sourceforge.dart index 50fc76a..5f04b0a 100644 --- a/lib/app_sources/sourceforge.dart +++ b/lib/app_sources/sourceforge.dart @@ -11,23 +11,27 @@ class SourceForge extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { var sourceRegex = getSourceRegex(hosts); - RegExp standardUrlRegExC = - RegExp('^https?://(www\\.)?$sourceRegex/p/.+', caseSensitive: false); + RegExp standardUrlRegExC = RegExp( + '^https?://(www\\.)?$sourceRegex/p/.+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegExC.firstMatch(url); if (match != null) { url = 'https://${Uri.parse(match.group(0)!).host}/projects/${url.substring(Uri.parse(match.group(0)!).host.length + '/projects/'.length + 1)}'; } RegExp standardUrlRegExB = RegExp( - '^https?://(www\\.)?$sourceRegex/projects/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?$sourceRegex/projects/[^/]+', + caseSensitive: false, + ); match = standardUrlRegExB.firstMatch(url); if (match != null && match.group(0) == url) { url = '$url/files'; } RegExp standardUrlRegExA = RegExp( - '^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?', - caseSensitive: false); + '^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?', + caseSensitive: false, + ); match = standardUrlRegExA.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -46,8 +50,9 @@ class SourceForge extends AppSource { standardUri = Uri.parse(standardUrl); } Response res = await sourceRequest( - '${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/', - additionalSettings); + '${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/', + additionalSettings, + ); if (res.statusCode == 200) { var parsedHtml = parse(res.body); var allDownloadLinks = parsedHtml @@ -74,9 +79,10 @@ class SourceForge extends AppSource { if (version != null) { try { var extractedVersion = extractVersion( - additionalSettings['versionExtractionRegEx'] as String?, - additionalSettings['matchGroupToUse'] as String?, - version); + additionalSettings['versionExtractionRegEx'] as String?, + additionalSettings['matchGroupToUse'] as String?, + version, + ); if (extractedVersion != null) { version = extractedVersion; } @@ -111,8 +117,11 @@ class SourceForge extends AppSource { .where((element) => getVersion(element) == version) .toList(); var segments = standardUrl.split('/'); - return APKDetails(version, getApkUrlsFromUrls(apkUrlList), - AppNames(name, segments[segments.indexOf('files') - 1])); + return APKDetails( + version, + getApkUrlsFromUrls(apkUrlList), + AppNames(name, segments[segments.indexOf('files') - 1]), + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/sourcehut.dart b/lib/app_sources/sourcehut.dart index fbe423d..4aa7448 100644 --- a/lib/app_sources/sourcehut.dart +++ b/lib/app_sources/sourcehut.dart @@ -13,17 +13,21 @@ class SourceHut extends AppSource { additionalSourceAppSpecificSettingFormItems = [ [ - GeneratedFormSwitch('fallbackToOlderReleases', - label: tr('fallbackToOlderReleases'), defaultValue: true) - ] + GeneratedFormSwitch( + 'fallbackToOlderReleases', + label: tr('fallbackToOlderReleases'), + defaultValue: true, + ), + ], ]; } @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', - caseSensitive: false); + '^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -52,8 +56,10 @@ class SourceHut extends AppSource { String appName = standardUri.pathSegments.last; bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; - Response res = - await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings); + Response res = await sourceRequest( + '$standardUrl/refs/rss.xml', + additionalSettings, + ); if (res.statusCode == 200) { var parsedHtml = parse(res.body); List apkDetailsList = []; @@ -63,10 +69,10 @@ class SourceHut extends AppSource { ind++; String releasePage = // querySelector('link') fails for some reason entry - .querySelector('guid') // Luckily guid is identical - ?.innerHtml - .trim() ?? - ''; + .querySelector('guid') // Luckily guid is identical + ?.innerHtml + .trim() ?? + ''; if (!releasePage.startsWith('$standardUrl/refs')) { continue; } @@ -84,8 +90,9 @@ class SourceHut extends AppSource { ? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString) : null; releaseDate = releaseDateString != null - ? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z') - .parse(releaseDateString) + ? DateFormat( + 'EEE, dd MMM yyyy HH:mm:ss Z', + ).parse(releaseDateString) : null; } catch (e) { // ignore @@ -93,27 +100,35 @@ class SourceHut extends AppSource { var res2 = await sourceRequest(releasePage, additionalSettings); List> apkUrls = []; if (res2.statusCode == 200) { - apkUrls = getApkUrlsFromUrls(parse(res2.body) - .querySelectorAll('a') - .map((e) => e.attributes['href'] ?? '') - .where((e) => e.toLowerCase().endsWith('.apk')) - .map((e) => ensureAbsoluteUrl(e, standardUri)) - .toList()); + apkUrls = getApkUrlsFromUrls( + parse(res2.body) + .querySelectorAll('a') + .map((e) => e.attributes['href'] ?? '') + .where((e) => e.toLowerCase().endsWith('.apk')) + .map((e) => ensureAbsoluteUrl(e, standardUri)) + .toList(), + ); } - apkDetailsList.add(APKDetails( + apkDetailsList.add( + APKDetails( version, apkUrls, - AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName, - appName), - releaseDate: releaseDate)); + AppNames( + entry.querySelector('author')?.innerHtml.trim() ?? appName, + appName, + ), + releaseDate: releaseDate, + ), + ); } if (apkDetailsList.isEmpty) { throw NoReleasesError(); } if (fallbackToOlderReleases) { if (additionalSettings['trackOnly'] != true) { - apkDetailsList = - apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList(); + apkDetailsList = apkDetailsList + .where((e) => e.apkUrls.isNotEmpty) + .toList(); } if (apkDetailsList.isEmpty) { throw NoReleasesError(); diff --git a/lib/app_sources/telegramapp.dart b/lib/app_sources/telegramapp.dart index 3bdf307..943cb4c 100644 --- a/lib/app_sources/telegramapp.dart +++ b/lib/app_sources/telegramapp.dart @@ -20,12 +20,15 @@ class TelegramApp extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = - await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings); + Response res = await sourceRequest( + 'https://t.me/s/TAndroidAPK', + additionalSettings, + ); if (res.statusCode == 200) { var http = parse(res.body); - var messages = - http.querySelectorAll('.tgme_widget_message_text.js-message_text'); + var messages = http.querySelectorAll( + '.tgme_widget_message_text.js-message_text', + ); var version = messages.isNotEmpty ? messages.last.innerHtml.split('\n').first.trim().split(' ').first : null; @@ -33,10 +36,9 @@ class TelegramApp extends AppSource { throw NoVersionError(); } String? apkUrl = 'https://telegram.org/dl/android/apk'; - return APKDetails( - version, - [MapEntry('telegram-$version.apk', apkUrl)], - AppNames('Telegram', 'Telegram')); + return APKDetails(version, [ + MapEntry('telegram-$version.apk', apkUrl), + ], AppNames('Telegram', 'Telegram')); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/tencent.dart b/lib/app_sources/tencent.dart index 3e85301..1fa3ac0 100644 --- a/lib/app_sources/tencent.dart +++ b/lib/app_sources/tencent.dart @@ -15,8 +15,9 @@ class Tencent extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://${getSourceRegex(hosts)}/appdetail/[^/]+', - caseSensitive: false); + '^https?://${getSourceRegex(hosts)}/appdetail/[^/]+', + caseSensitive: false, + ); var match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -25,8 +26,10 @@ class Tencent extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return Uri.parse(standardUrl).pathSegments.last; } @@ -36,18 +39,16 @@ class Tencent extends AppSource { Map additionalSettings, ) async { String appId = (await tryInferringAppId(standardUrl))!; - String baseHost = Uri.parse(standardUrl) - .host - .split('.') - .reversed - .toList() - .sublist(0, 2) - .reversed - .join('.'); + String baseHost = Uri.parse( + standardUrl, + ).host.split('.').reversed.toList().sublist(0, 2).reversed.join('.'); var res = await sourceRequest( - 'https://upage.html5.$baseHost/wechat-apkinfo', additionalSettings, - followRedirects: false, postBody: {"packagename": appId}); + 'https://upage.html5.$baseHost/wechat-apkinfo', + additionalSettings, + followRedirects: false, + postBody: {"packagename": appId}, + ); if (res.statusCode == 200) { var json = jsonDecode(res.body); @@ -64,14 +65,18 @@ class Tencent extends AppSource { var author = json['app_detail_records'][appId]['app_info']['author']; var releaseDate = json['app_detail_records'][appId]['app_info']['update_time']; - var apkName = Uri.parse(apkUrl).queryParameters['fsname'] ?? + var apkName = + Uri.parse(apkUrl).queryParameters['fsname'] ?? '${appId}_$version.apk'; return APKDetails( - version, [MapEntry(apkName, apkUrl)], AppNames(author, appName), - releaseDate: releaseDate != null - ? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000) - : null); + version, + [MapEntry(apkName, apkUrl)], + AppNames(author, appName), + releaseDate: releaseDate != null + ? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000) + : null, + ); } else { throw getObtainiumHttpError(res); } diff --git a/lib/app_sources/uptodown.dart b/lib/app_sources/uptodown.dart index 134b64c..3dbee4c 100644 --- a/lib/app_sources/uptodown.dart +++ b/lib/app_sources/uptodown.dart @@ -30,8 +30,9 @@ class Uptodown extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( - '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', - caseSensitive: false); + '^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}', + caseSensitive: false, + ); RegExpMatch? match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); @@ -40,14 +41,20 @@ class Uptodown extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return (await getAppDetailsFromPage( - standardUrl, additionalSettings))['appId']; + standardUrl, + additionalSettings, + ))['appId']; } Future> getAppDetailsFromPage( - String standardUrl, Map additionalSettings) async { + String standardUrl, + Map additionalSettings, + ) async { var res = await sourceRequest(standardUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); @@ -63,8 +70,9 @@ class Uptodown extends AppSource { .toList(); String? appId = detailElements.elementAtOrNull(0); String? dateStr = detailElements.elementAtOrNull(6); - String? fileId = - html.querySelector('#detail-app-name')?.attributes['data-file-id']; + String? fileId = html + .querySelector('#detail-app-name') + ?.attributes['data-file-id']; String? extension = detailElements.elementAtOrNull(7)?.toLowerCase(); return Map.fromEntries([ MapEntry('version', version), @@ -73,7 +81,7 @@ class Uptodown extends AppSource { MapEntry('author', author), MapEntry('dateStr', dateStr), MapEntry('fileId', fileId), - MapEntry('extension', extension) + MapEntry('extension', extension), ]); } @@ -82,8 +90,10 @@ class Uptodown extends AppSource { String standardUrl, Map additionalSettings, ) async { - var appDetails = - await getAppDetailsFromPage(standardUrl, additionalSettings); + var appDetails = await getAppDetailsFromPage( + standardUrl, + additionalSettings, + ); var version = appDetails['version']; var appId = appDetails['appId']; var fileId = appDetails['fileId']; @@ -105,21 +115,28 @@ class Uptodown extends AppSource { if (dateStr != null) { relDate = parseDateTimeMMMddCommayyyy(dateStr); } - return APKDetails(version, [MapEntry('$appId.$extension', apkUrl)], - AppNames(author, appName), - releaseDate: relDate); + return APKDetails( + version, + [MapEntry('$appId.$extension', apkUrl)], + AppNames(author, appName), + releaseDate: relDate, + ); } @override - Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, - Map additionalSettings) async { + Future apkUrlPrefetchModifier( + String apkUrl, + String standardUrl, + Map additionalSettings, + ) async { var res = await sourceRequest(apkUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } var html = parse(res.body); - var finalUrlKey = - html.querySelector('#detail-download-button')?.attributes['data-url']; + var finalUrlKey = html + .querySelector('#detail-download-button') + ?.attributes['data-url']; if (finalUrlKey == null) { throw NoAPKError(); } diff --git a/lib/app_sources/vivoappstore.dart b/lib/app_sources/vivoappstore.dart index 68f41cd..278ca3c 100644 --- a/lib/app_sources/vivoappstore.dart +++ b/lib/app_sources/vivoappstore.dart @@ -23,15 +23,19 @@ class VivoAppStore extends AppSource { } @override - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { var json = await getDetailJson(standardUrl, additionalSettings); return json['package_name']; } @override Future getLatestAPKDetails( - String standardUrl, Map additionalSettings) async { + String standardUrl, + Map additionalSettings, + ) async { var json = await getDetailJson(standardUrl, additionalSettings); var appName = json['title_zh'].toString(); var packageName = json['package_name'].toString(); @@ -42,13 +46,18 @@ class VivoAppStore extends AppSource { var apkUrl = json['download_url'].toString(); var apkName = '${packageName}_$versionCode.apk'; return APKDetails( - versionName, [MapEntry(apkName, apkUrl)], AppNames(developer, appName), - releaseDate: DateTime.parse(uploadTime)); + versionName, + [MapEntry(apkName, apkUrl)], + AppNames(developer, appName), + releaseDate: DateTime.parse(uploadTime), + ); } @override - Future>> search(String query, - {Map querySettings = const {}}) async { + Future>> search( + String query, { + Map querySettings = const {}, + }) async { var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/h5appstore/search/result-list?app_version=2100&page_index=1&apps_per_page=20&target=local&cfrom=2&key='; var searchUrl = '$apiBaseUrl${Uri.encodeQueryComponent(query)}'; @@ -65,14 +74,16 @@ class VivoAppStore extends AppSource { for (var item in (resultsJson as List)) { results['$appDetailUrl${item['id']}'] = [ item['title_zh'].toString(), - item['developer'].toString() + item['developer'].toString(), ]; } return results; } Future> getDetailJson( - String standardUrl, Map additionalSettings) async { + String standardUrl, + Map additionalSettings, + ) async { var vivoAppId = parseVivoAppId(standardUrl); var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/'; var params = '?frompage=messageh5&app_version=2100'; diff --git a/lib/components/custom_app_bar.dart b/lib/components/custom_app_bar.dart index 47e08d2..425883f 100644 --- a/lib/components/custom_app_bar.dart +++ b/lib/components/custom_app_bar.dart @@ -20,8 +20,9 @@ class _CustomAppBarState extends State { titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), title: Text( widget.title, - style: - TextStyle(color: Theme.of(context).textTheme.bodyMedium!.color), + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium!.color, + ), ), ), ); diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index e9dcc0b..27c2db5 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -16,11 +16,13 @@ abstract class GeneratedFormItem { dynamic ensureType(dynamic val); GeneratedFormItem clone(); - GeneratedFormItem(this.key, - {this.label = 'Input', - this.belowWidgets = const [], - this.defaultValue, - this.additionalValidators = const []}); + GeneratedFormItem( + this.key, { + this.label = 'Input', + this.belowWidgets = const [], + this.defaultValue, + this.additionalValidators = const [], + }); } class GeneratedFormTextField extends GeneratedFormItem { @@ -31,18 +33,19 @@ class GeneratedFormTextField extends GeneratedFormItem { late TextInputType? textInputType; late List? autoCompleteOptions; - GeneratedFormTextField(super.key, - {super.label, - super.belowWidgets, - String super.defaultValue = '', - List super.additionalValidators = - const [], - this.required = true, - this.max = 1, - this.hint, - this.password = false, - this.textInputType, - this.autoCompleteOptions}); + GeneratedFormTextField( + super.key, { + super.label, + super.belowWidgets, + String super.defaultValue = '', + List super.additionalValidators = const [], + this.required = true, + this.max = 1, + this.hint, + this.password = false, + this.textInputType, + this.autoCompleteOptions, + }); @override String ensureType(val) { @@ -51,16 +54,18 @@ class GeneratedFormTextField extends GeneratedFormItem { @override GeneratedFormTextField clone() { - return GeneratedFormTextField(key, - label: label, - belowWidgets: belowWidgets, - defaultValue: defaultValue, - additionalValidators: List.from(additionalValidators), - required: required, - max: max, - hint: hint, - password: password, - textInputType: textInputType); + return GeneratedFormTextField( + key, + label: label, + belowWidgets: belowWidgets, + defaultValue: defaultValue, + additionalValidators: List.from(additionalValidators), + required: required, + max: max, + hint: hint, + password: password, + textInputType: textInputType, + ); } } @@ -91,8 +96,9 @@ class GeneratedFormDropdown extends GeneratedFormItem { label: label, belowWidgets: belowWidgets, defaultValue: defaultValue, - disabledOptKeys: - disabledOptKeys != null ? List.from(disabledOptKeys!) : null, + disabledOptKeys: disabledOptKeys != null + ? List.from(disabledOptKeys!) + : null, additionalValidators: List.from(additionalValidators), ); } @@ -117,12 +123,14 @@ class GeneratedFormSwitch extends GeneratedFormItem { @override GeneratedFormSwitch clone() { - return GeneratedFormSwitch(key, - label: label, - belowWidgets: belowWidgets, - defaultValue: defaultValue, - disabled: false, - additionalValidators: List.from(additionalValidators)); + return GeneratedFormSwitch( + key, + label: label, + belowWidgets: belowWidgets, + defaultValue: defaultValue, + disabled: false, + additionalValidators: List.from(additionalValidators), + ); } } @@ -132,17 +140,20 @@ class GeneratedFormTagInput extends GeneratedFormItem { late WrapAlignment alignment; late String emptyMessage; late bool showLabelWhenNotEmpty; - GeneratedFormTagInput(super.key, - {super.label, - super.belowWidgets, - Map> super.defaultValue = const {}, - List> value)> - super.additionalValidators = const [], - this.deleteConfirmationMessage, - this.singleSelect = false, - this.alignment = WrapAlignment.start, - this.emptyMessage = 'Input', - this.showLabelWhenNotEmpty = true}); + GeneratedFormTagInput( + super.key, { + super.label, + super.belowWidgets, + Map> super.defaultValue = const {}, + List> value)> + super.additionalValidators = + const [], + this.deleteConfirmationMessage, + this.singleSelect = false, + this.alignment = WrapAlignment.start, + this.emptyMessage = 'Input', + this.showLabelWhenNotEmpty = true, + }); @override Map> ensureType(val) { @@ -151,25 +162,30 @@ class GeneratedFormTagInput extends GeneratedFormItem { @override GeneratedFormTagInput clone() { - return GeneratedFormTagInput(key, - label: label, - belowWidgets: belowWidgets, - defaultValue: defaultValue, - additionalValidators: List.from(additionalValidators), - deleteConfirmationMessage: deleteConfirmationMessage, - singleSelect: singleSelect, - alignment: alignment, - emptyMessage: emptyMessage, - showLabelWhenNotEmpty: showLabelWhenNotEmpty); + return GeneratedFormTagInput( + key, + label: label, + belowWidgets: belowWidgets, + defaultValue: defaultValue, + additionalValidators: List.from(additionalValidators), + deleteConfirmationMessage: deleteConfirmationMessage, + singleSelect: singleSelect, + alignment: alignment, + emptyMessage: emptyMessage, + showLabelWhenNotEmpty: showLabelWhenNotEmpty, + ); } } -typedef OnValueChanges = void Function( - Map values, bool valid, bool isBuilding); +typedef OnValueChanges = + void Function(Map values, bool valid, bool isBuilding); class GeneratedForm extends StatefulWidget { - const GeneratedForm( - {super.key, required this.items, required this.onValueChanges}); + const GeneratedForm({ + super.key, + required this.items, + required this.onValueChanges, + }); final List> items; final OnValueChanges onValueChanges; @@ -179,7 +195,8 @@ class GeneratedForm extends StatefulWidget { } List> cloneFormItems( - List> items) { + List> items, +) { List> clonedItems = []; for (var row in items) { List clonedRow = []; @@ -194,8 +211,13 @@ List> cloneFormItems( class GeneratedFormSubForm extends GeneratedFormItem { final List> items; - GeneratedFormSubForm(super.key, this.items, - {super.label, super.belowWidgets, super.defaultValue = const []}); + GeneratedFormSubForm( + super.key, + this.items, { + super.label, + super.belowWidgets, + super.defaultValue = const [], + }); @override ensureType(val) { @@ -204,8 +226,13 @@ class GeneratedFormSubForm extends GeneratedFormItem { @override GeneratedFormSubForm clone() { - return GeneratedFormSubForm(key, cloneFormItems(items), - label: label, belowWidgets: belowWidgets, defaultValue: defaultValue); + return GeneratedFormSubForm( + key, + cloneFormItems(items), + label: label, + belowWidgets: belowWidgets, + defaultValue: defaultValue, + ); } } @@ -220,13 +247,18 @@ Color generateRandomLightColor() { // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 final List rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); // Map RBG values from 0-1 to 0-255: - final List rgbValues = - rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); + final List rgbValues = rgbValuesDbl + .map((rgb) => (rgb * 255).toInt()) + .toList(); return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); } -int generateRandomNumber(int seed1, - {int seed2 = 0, int seed3 = 0, max = 10000}) { +int generateRandomNumber( + int seed1, { + int seed2 = 0, + int seed3 = 0, + max = 10000, +}) { int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode; Random random = Random(combinedSeed); int randomNumber = random.nextInt(max); @@ -297,9 +329,9 @@ class _GeneratedFormState extends State { }); }, decoration: InputDecoration( - helperText: - formItem.label + (formItem.required ? ' *' : ''), - hintText: formItem.hint), + helperText: formItem.label + (formItem.required ? ' *' : ''), + hintText: formItem.hint, + ), minLines: formItem.max <= 1 ? null : formItem.max, maxLines: formItem.max <= 1 ? 1 : formItem.max, validator: (value) { @@ -339,23 +371,26 @@ class _GeneratedFormState extends State { return Text(tr('dropdownNoOptsError')); } return DropdownButtonFormField( - decoration: InputDecoration(labelText: formItem.label), - value: values[formItem.key], - items: formItem.opts!.map((e2) { - var enabled = - formItem.disabledOptKeys?.contains(e2.key) != true; - return DropdownMenuItem( - value: e2.key, - enabled: enabled, - child: Opacity( - opacity: enabled ? 1 : 0.5, child: Text(e2.value))); - }).toList(), - onChanged: (value) { - setState(() { - values[formItem.key] = value ?? formItem.opts!.first.key; - someValueChanged(); - }); + decoration: InputDecoration(labelText: formItem.label), + value: values[formItem.key], + items: formItem.opts!.map((e2) { + var enabled = formItem.disabledOptKeys?.contains(e2.key) != true; + return DropdownMenuItem( + value: e2.key, + enabled: enabled, + child: Opacity( + opacity: enabled ? 1 : 0.5, + child: Text(e2.value), + ), + ); + }).toList(), + onChanged: (value) { + setState(() { + values[formItem.key] = value ?? formItem.opts!.first.key; + someValueChanged(); }); + }, + ); } else if (formItem is GeneratedFormSubForm) { values[formItem.key] = []; for (Map v @@ -394,33 +429,33 @@ class _GeneratedFormState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(widget.items[r][e].label)), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), Switch( - value: values[fieldKey], - onChanged: - (widget.items[r][e] as GeneratedFormSwitch).disabled - ? null - : (value) { - setState(() { - values[fieldKey] = value; - someValueChanged(); - }); - }) + value: values[fieldKey], + onChanged: (widget.items[r][e] as GeneratedFormSwitch).disabled + ? null + : (value) { + setState(() { + values[fieldKey] = value; + someValueChanged(); + }); + }, + ), ], ); } else if (widget.items[r][e] is GeneratedFormTagInput) { onAddPressed() { showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: widget.items[r][e].label, - items: [ - [GeneratedFormTextField('label', label: tr('label'))] - ]); - }).then((value) { + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: widget.items[r][e].label, + items: [ + [GeneratedFormTextField('label', label: tr('label'))], + ], + ); + }, + ).then((value) { String? label = value?['label']; if (label != null) { setState(() { @@ -434,8 +469,10 @@ class _GeneratedFormState extends State { var someSelected = temp.entries .where((element) => element.value.value) .isNotEmpty; - temp[label] = MapEntry(generateRandomLightColor().value, - !(someSelected && singleSelect)); + temp[label] = MapEntry( + generateRandomLightColor().value, + !(someSelected && singleSelect), + ); values[fieldKey] = temp; someValueChanged(); } @@ -444,236 +481,274 @@ class _GeneratedFormState extends State { }); } - formInputs[r][e] = - Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if ((values[fieldKey] as Map>?) - ?.isNotEmpty == - true && - (widget.items[r][e] as GeneratedFormTagInput) - .showLabelWhenNotEmpty) - Column( - crossAxisAlignment: - (widget.items[r][e] as GeneratedFormTagInput).alignment == - WrapAlignment.center - ? CrossAxisAlignment.center - : CrossAxisAlignment.stretch, + formInputs[r][e] = Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if ((values[fieldKey] as Map>?) + ?.isNotEmpty == + true && + (widget.items[r][e] as GeneratedFormTagInput) + .showLabelWhenNotEmpty) + Column( + crossAxisAlignment: + (widget.items[r][e] as GeneratedFormTagInput).alignment == + WrapAlignment.center + ? CrossAxisAlignment.center + : CrossAxisAlignment.stretch, + children: [ + Text(widget.items[r][e].label), + const SizedBox(height: 8), + ], + ), + Wrap( + alignment: + (widget.items[r][e] as GeneratedFormTagInput).alignment, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - Text(widget.items[r][e].label), - const SizedBox( - height: 8, - ), - ], - ), - Wrap( - alignment: - (widget.items[r][e] as GeneratedFormTagInput).alignment, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - // (values[fieldKey] as Map>?) - // ?.isEmpty == - // true - // ? Text( - // (widget.items[r][e] as GeneratedFormTagInput) - // .emptyMessage, - // ) - // : const SizedBox.shrink(), - ...(values[fieldKey] as Map>?) - ?.entries - .map((e2) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ChoiceChip( - label: Text(e2.key), - backgroundColor: Color(e2.value.key).withAlpha(50), - selectedColor: Color(e2.value.key), - visualDensity: VisualDensity.compact, - selected: e2.value.value, - onSelected: (value) { - setState(() { - (values[fieldKey] as Map>)[e2.key] = - MapEntry( - (values[fieldKey] as Map>)[e2.key]! - .key, - value); - if ((widget.items[r][e] - as GeneratedFormTagInput) - .singleSelect && - value == true) { - for (var key in (values[fieldKey] - as Map>) - .keys) { - if (key != e2.key) { - (values[fieldKey] as Map< - String, - MapEntry>)[key] = MapEntry( - (values[fieldKey] as Map>)[key]! - .key, - false); + // (values[fieldKey] as Map>?) + // ?.isEmpty == + // true + // ? Text( + // (widget.items[r][e] as GeneratedFormTagInput) + // .emptyMessage, + // ) + // : const SizedBox.shrink(), + ...(values[fieldKey] as Map>?) + ?.entries + .map((e2) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + child: ChoiceChip( + label: Text(e2.key), + backgroundColor: Color( + e2.value.key, + ).withAlpha(50), + selectedColor: Color(e2.value.key), + visualDensity: VisualDensity.compact, + selected: e2.value.value, + onSelected: (value) { + setState(() { + (values[fieldKey] + as Map>)[e2 + .key] = MapEntry( + (values[fieldKey] + as Map< + String, + MapEntry + >)[e2.key]! + .key, + value, + ); + if ((widget.items[r][e] + as GeneratedFormTagInput) + .singleSelect && + value == true) { + for (var key + in (values[fieldKey] + as Map< + String, + MapEntry + >) + .keys) { + if (key != e2.key) { + (values[fieldKey] + as Map< + String, + MapEntry + >)[key] = MapEntry( + (values[fieldKey] + as Map< + String, + MapEntry + >)[key]! + .key, + false, + ); + } + } } - } - } - someValueChanged(); - }); - }, - )); - }) ?? - [const SizedBox.shrink()], - (values[fieldKey] as Map>?) - ?.values - .where((e) => e.value) - .length == - 1 - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: IconButton( - onPressed: () { - setState(() { - var temp = values[fieldKey] - as Map>; - // get selected category str where bool is true - final oldEntry = temp.entries - .firstWhere((entry) => entry.value.value); - // generate new color, ensure it is not the same - int newColor = oldEntry.value.key; - while (oldEntry.value.key == newColor) { - newColor = generateRandomLightColor().value; - } - // Update entry with new color, remain selected - temp.update(oldEntry.key, - (old) => MapEntry(newColor, old.value)); - values[fieldKey] = temp; - someValueChanged(); - }); - }, - icon: const Icon(Icons.format_color_fill_rounded), - visualDensity: VisualDensity.compact, - tooltip: tr('colour'), - )) - : const SizedBox.shrink(), - (values[fieldKey] as Map>?) - ?.values - .where((e) => e.value) - .isNotEmpty == - true - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: IconButton( - onPressed: () { - fn() { + someValueChanged(); + }); + }, + ), + ); + }) ?? + [const SizedBox.shrink()], + (values[fieldKey] as Map>?) + ?.values + .where((e) => e.value) + .length == + 1 + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: IconButton( + onPressed: () { setState(() { - var temp = values[fieldKey] - as Map>; - temp.removeWhere((key, value) => value.value); + var temp = + values[fieldKey] + as Map>; + // get selected category str where bool is true + final oldEntry = temp.entries.firstWhere( + (entry) => entry.value.value, + ); + // generate new color, ensure it is not the same + int newColor = oldEntry.value.key; + while (oldEntry.value.key == newColor) { + newColor = generateRandomLightColor().value; + } + // Update entry with new color, remain selected + temp.update( + oldEntry.key, + (old) => MapEntry(newColor, old.value), + ); values[fieldKey] = temp; someValueChanged(); }); - } + }, + icon: const Icon(Icons.format_color_fill_rounded), + visualDensity: VisualDensity.compact, + tooltip: tr('colour'), + ), + ) + : const SizedBox.shrink(), + (values[fieldKey] as Map>?) + ?.values + .where((e) => e.value) + .isNotEmpty == + true + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: IconButton( + onPressed: () { + fn() { + setState(() { + var temp = + values[fieldKey] + as Map>; + temp.removeWhere((key, value) => value.value); + values[fieldKey] = temp; + someValueChanged(); + }); + } - if ((widget.items[r][e] as GeneratedFormTagInput) - .deleteConfirmationMessage != - null) { - var message = - (widget.items[r][e] as GeneratedFormTagInput) - .deleteConfirmationMessage!; - showDialog?>( + if ((widget.items[r][e] as GeneratedFormTagInput) + .deleteConfirmationMessage != + null) { + var message = + (widget.items[r][e] + as GeneratedFormTagInput) + .deleteConfirmationMessage!; + showDialog?>( context: context, builder: (BuildContext ctx) { return GeneratedFormModal( - title: message.key, - message: message.value, - items: const []); - }).then((value) { - if (value != null) { - fn(); - } - }); - } else { - fn(); - } - }, - icon: const Icon(Icons.remove), - visualDensity: VisualDensity.compact, - tooltip: tr('remove'), - )) - : const SizedBox.shrink(), - (values[fieldKey] as Map>?) - ?.isEmpty == - true - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: TextButton.icon( - onPressed: onAddPressed, - icon: const Icon(Icons.add), - label: Text( + title: message.key, + message: message.value, + items: const [], + ); + }, + ).then((value) { + if (value != null) { + fn(); + } + }); + } else { + fn(); + } + }, + icon: const Icon(Icons.remove), + visualDensity: VisualDensity.compact, + tooltip: tr('remove'), + ), + ) + : const SizedBox.shrink(), + (values[fieldKey] as Map>?) + ?.isEmpty == + true + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: TextButton.icon( + onPressed: onAddPressed, + icon: const Icon(Icons.add), + label: Text( (widget.items[r][e] as GeneratedFormTagInput) - .label), - )) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: IconButton( - onPressed: onAddPressed, - icon: const Icon(Icons.add), - visualDensity: VisualDensity.compact, - tooltip: tr('add'), - )), - ], - ) - ]); + .label, + ), + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: IconButton( + onPressed: onAddPressed, + icon: const Icon(Icons.add), + visualDensity: VisualDensity.compact, + tooltip: tr('add'), + ), + ), + ], + ), + ], + ); } else if (widget.items[r][e] is GeneratedFormSubForm) { List subformColumn = []; - var compact = (widget.items[r][e] as GeneratedFormSubForm) - .items - .length == - 1 && + var compact = + (widget.items[r][e] as GeneratedFormSubForm).items.length == 1 && (widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1; for (int i = 0; i < values[fieldKey].length; i++) { - var internalFormKey = ValueKey(generateRandomNumber( + var internalFormKey = ValueKey( + generateRandomNumber( values[fieldKey].length, seed2: i, - seed3: forceUpdateKeyCount)); - subformColumn.add(Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!compact) - const SizedBox( - height: 16, + seed3: forceUpdateKeyCount, + ), + ); + subformColumn.add( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!compact) const SizedBox(height: 16), + if (!compact) + Text( + '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + GeneratedForm( + key: internalFormKey, + items: + cloneFormItems( + (widget.items[r][e] as GeneratedFormSubForm) + .items, + ) + .map( + (x) => x.map((y) { + y.defaultValue = values[fieldKey]?[i]?[y.key]; + y.key = '${y.key.toString()},$internalFormKey'; + return y; + }).toList(), + ) + .toList(), + onValueChanges: (values, valid, isBuilding) { + values = values.map( + (key, value) => MapEntry(key.split(',')[0], value), + ); + if (valid) { + this.values[fieldKey]?[i] = values; + } + someValueChanged( + isBuilding: isBuilding, + forceInvalid: !valid, + ); + }, ), - if (!compact) - Text( - '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - GeneratedForm( - key: internalFormKey, - items: cloneFormItems( - (widget.items[r][e] as GeneratedFormSubForm).items) - .map((x) => x.map((y) { - y.defaultValue = values[fieldKey]?[i]?[y.key]; - y.key = '${y.key.toString()},$internalFormKey'; - return y; - }).toList()) - .toList(), - onValueChanges: (values, valid, isBuilding) { - values = values.map( - (key, value) => MapEntry(key.split(',')[0], value)); - if (valid) { - this.values[fieldKey]?[i] = values; - } - someValueChanged( - isBuilding: isBuilding, forceInvalid: !valid); - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.error), + foregroundColor: Theme.of(context).colorScheme.error, + ), onPressed: (values[fieldKey].length > 0) ? () { var temp = List.from(values[fieldKey]); @@ -686,33 +761,40 @@ class _GeneratedFormState extends State { label: Text( '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})', ), - icon: const Icon( - Icons.delete_outline_rounded, - )) - ], - ) - ], - )); + icon: const Icon(Icons.delete_outline_rounded), + ), + ], + ), + ], + ), + ); } - subformColumn.add(Padding( - padding: const EdgeInsets.only(bottom: 0, top: 8), - child: Row( - children: [ - Expanded( + subformColumn.add( + Padding( + padding: const EdgeInsets.only(bottom: 0, top: 8), + child: Row( + children: [ + Expanded( child: ElevatedButton.icon( - onPressed: () { - values[fieldKey].add(getDefaultValuesFromFormItems( - (widget.items[r][e] as GeneratedFormSubForm) - .items)); - forceUpdateKeyCount++; - someValueChanged(); - }, - icon: const Icon(Icons.add), - label: Text((widget.items[r][e] as GeneratedFormSubForm) - .label))), - ], + onPressed: () { + values[fieldKey].add( + getDefaultValuesFromFormItems( + (widget.items[r][e] as GeneratedFormSubForm).items, + ), + ); + forceUpdateKeyCount++; + someValueChanged(); + }, + icon: const Icon(Icons.add), + label: Text( + (widget.items[r][e] as GeneratedFormSubForm).label, + ), + ), + ), + ], + ), ), - )); + ); formInputs[r][e] = Column(children: subformColumn); } } @@ -726,38 +808,43 @@ class _GeneratedFormState extends State { height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch ? 8 : 25, - ) + ), ]); } List rowItems = []; rowInputs.value.asMap().entries.forEach((rowInput) { if (rowInput.key > 0) { - rowItems.add(const SizedBox( - width: 20, - )); + rowItems.add(const SizedBox(width: 20)); } - rowItems.add(Expanded( + rowItems.add( + Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - rowInput.value, - ...widget.items[rowInputs.key][rowInput.key].belowWidgets - ]))); + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + rowInput.value, + ...widget.items[rowInputs.key][rowInput.key].belowWidgets, + ], + ), + ), + ); }); rows.add(rowItems); }); return Form( - key: _formKey, - child: Column( - children: [ - ...rows.map((row) => Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [...row.map((e) => e)], - )) - ], - )); + key: _formKey, + child: Column( + children: [ + ...rows.map( + (row) => Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [...row.map((e) => e)], + ), + ), + ], + ), + ); } } diff --git a/lib/components/generated_form_modal.dart b/lib/components/generated_form_modal.dart index 838b187..118cba2 100644 --- a/lib/components/generated_form_modal.dart +++ b/lib/components/generated_form_modal.dart @@ -4,15 +4,16 @@ import 'package:flutter/services.dart'; import 'package:obtainium/components/generated_form.dart'; class GeneratedFormModal extends StatefulWidget { - const GeneratedFormModal( - {super.key, - required this.title, - required this.items, - this.initValid = false, - this.message = '', - this.additionalWidgets = const [], - this.singleNullReturnButton, - this.primaryActionColour}); + const GeneratedFormModal({ + super.key, + required this.title, + required this.items, + this.initValid = false, + this.message = '', + this.additionalWidgets = const [], + this.singleNullReturnButton, + this.primaryActionColour, + }); final String title; final String message; @@ -41,14 +42,12 @@ class _GeneratedFormModalState extends State { return AlertDialog( scrollable: true, title: Text(widget.title), - content: - Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.message.isNotEmpty) Text(widget.message), - if (widget.message.isNotEmpty) - const SizedBox( - height: 16, - ), - GeneratedForm( + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.message.isNotEmpty) Text(widget.message), + if (widget.message.isNotEmpty) const SizedBox(height: 16), + GeneratedForm( items: widget.items, onValueChanges: (values, valid, isBuilding) { if (isBuilding) { @@ -60,23 +59,29 @@ class _GeneratedFormModalState extends State { this.valid = valid; }); } - }), - if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets - ]), + }, + ), + if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets, + ], + ), actions: [ TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(widget.singleNullReturnButton == null + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text( + widget.singleNullReturnButton == null ? tr('cancel') - : widget.singleNullReturnButton!)), + : widget.singleNullReturnButton!, + ), + ), widget.singleNullReturnButton == null ? TextButton( style: widget.primaryActionColour == null ? null : TextButton.styleFrom( - foregroundColor: widget.primaryActionColour), + foregroundColor: widget.primaryActionColour, + ), onPressed: !valid ? null : () { @@ -85,8 +90,9 @@ class _GeneratedFormModalState extends State { Navigator.of(context).pop(values); } }, - child: Text(tr('continue'))) - : const SizedBox.shrink() + child: Text(tr('continue')), + ) + : const SizedBox.shrink(), ], ); } diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index 529f5fa..0ecdf74 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -20,23 +20,24 @@ class ObtainiumError { class RateLimitError extends ObtainiumError { late int remainingMinutes; RateLimitError(this.remainingMinutes) - : super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes)); + : super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes)); } class InvalidURLError extends ObtainiumError { InvalidURLError(String sourceName) - : super(tr('invalidURLForSource', args: [sourceName])); + : super(tr('invalidURLForSource', args: [sourceName])); } class CredsNeededError extends ObtainiumError { CredsNeededError(String sourceName) - : super(tr('requiresCredentialsInSettings', args: [sourceName])); + : super(tr('requiresCredentialsInSettings', args: [sourceName])); } class NoReleasesError extends ObtainiumError { NoReleasesError({String? note}) - : super( - '${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}'); + : super( + '${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}', + ); } class NoAPKError extends ObtainiumError { @@ -57,7 +58,7 @@ class DowngradeError extends ObtainiumError { class InstallError extends ObtainiumError { InstallError(int code) - : super(PackageInstallerStatus.byCode(code).name.substring(7)); + : super(PackageInstallerStatus.byCode(code).name.substring(7)); } class IDChangedError extends ObtainiumError { @@ -93,8 +94,11 @@ class MultiAppMultiError extends ObtainiumError { String errorString(String appId, {bool includeIdsWithNames = false}) => '${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}'; - String errorsAppsString(String errString, List appIds, - {bool includeIdsWithNames = false}) => + String errorsAppsString( + String errString, + List appIds, { + bool includeIdsWithNames = false, + }) => '$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]'; @override @@ -104,38 +108,45 @@ class MultiAppMultiError extends ObtainiumError { } showMessage(dynamic e, BuildContext context, {bool isError = false}) { - Provider.of(context, listen: false) - .add(e.toString(), level: isError ? LogLevels.error : LogLevels.info); + Provider.of( + context, + listen: false, + ).add(e.toString(), level: isError ? LogLevels.error : LogLevels.info); if (e is String || (e is ObtainiumError && !e.unexpected)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); } else { showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - title: Text(e is MultiAppMultiError + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + title: Text( + e is MultiAppMultiError ? tr(isError ? 'someErrors' : 'updates') - : tr(isError ? 'unexpectedError' : 'unknown')), - content: GestureDetector( - onLongPress: () { - Clipboard.setData(ClipboardData(text: e.toString())); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(tr('copiedToClipboard')), - )); - }, - child: Text(e.toString())), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(tr('ok'))), - ], - ); - }); + : tr(isError ? 'unexpectedError' : 'unknown'), + ), + content: GestureDetector( + onLongPress: () { + Clipboard.setData(ClipboardData(text: e.toString())); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard')))); + }, + child: Text(e.toString()), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(tr('ok')), + ), + ], + ); + }, + ); } } @@ -147,14 +158,16 @@ String list2FriendlyString(List list) { return list.length == 2 ? '${list[0]} ${tr('and')} ${list[1]}' : list - .asMap() - .entries - .map((e) => - e.value + - (e.key == list.length - 1 - ? '' - : e.key == list.length - 2 + .asMap() + .entries + .map( + (e) => + e.value + + (e.key == list.length - 1 + ? '' + : e.key == list.length - 2 ? ' and ' - : ', ')) - .join(''); + : ', '), + ) + .join(''); } diff --git a/lib/main.dart b/lib/main.dart index b1e8953..10c00b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,8 +43,10 @@ List> supportedLocales = const [ MapEntry(Locale('tr'), 'Türkçe'), MapEntry(Locale('uk'), 'Українська'), MapEntry(Locale('da'), 'Dansk'), - MapEntry(Locale('en', 'EO'), - 'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493 + MapEntry( + Locale('en', 'EO'), + 'Esperanto', + ), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493 MapEntry(Locale('in'), 'Bahasa Indonesia'), MapEntry(Locale('ko'), '한국어'), MapEntry(Locale('ca'), 'Català'), @@ -76,9 +78,11 @@ Future loadTranslations() async { }, ); await controller.loadTranslations(); - Localization.load(controller.locale, - translations: controller.translations, - fallbackTranslations: controller.fallbackTranslations); + Localization.load( + controller.locale, + translations: controller.translations, + fallbackTranslations: controller.fallbackTranslations, + ); } @pragma('vm:entry-point') @@ -97,10 +101,12 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { void main() async { WidgetsFlutterBinding.ensureInitialized(); try { - ByteData data = - await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem'); - SecurityContext.defaultContext - .setTrustedCertificatesBytes(data.buffer.asUint8List()); + ByteData data = await PlatformAssetBundle().load( + 'assets/ca/lets-encrypt-r3.pem', + ); + SecurityContext.defaultContext.setTrustedCertificatesBytes( + data.buffer.asUint8List(), + ); } catch (e) { // Already added, do nothing (see #375) } @@ -113,20 +119,23 @@ void main() async { } final np = NotificationsProvider(); await np.initialize(); - runApp(MultiProvider( - providers: [ - ChangeNotifierProvider(create: (context) => AppsProvider()), - ChangeNotifierProvider(create: (context) => SettingsProvider()), - Provider(create: (context) => np), - Provider(create: (context) => LogsProvider()) - ], - child: EasyLocalization( + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => AppsProvider()), + ChangeNotifierProvider(create: (context) => SettingsProvider()), + Provider(create: (context) => np), + Provider(create: (context) => LogsProvider()), + ], + child: EasyLocalization( supportedLocales: supportedLocales.map((e) => e.key).toList(), path: localeDir, fallbackLocale: fallbackLocale, useOnlyLangCode: false, - child: const Obtainium()), - )); + child: const Obtainium(), + ), + ), + ); BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); } @@ -148,22 +157,26 @@ class _ObtainiumState extends State { Future initPlatformState() async { await BackgroundFetch.configure( - BackgroundFetchConfig( - minimumFetchInterval: 15, - stopOnTerminate: false, - startOnBoot: true, - enableHeadless: true, - requiresBatteryNotLow: false, - requiresCharging: false, - requiresStorageNotLow: false, - requiresDeviceIdle: false, - requiredNetworkType: NetworkType.ANY), (String taskId) async { - await bgUpdateCheck(taskId, null); - BackgroundFetch.finish(taskId); - }, (String taskId) async { - context.read().add('BG update task timed out.'); - BackgroundFetch.finish(taskId); - }); + BackgroundFetchConfig( + minimumFetchInterval: 15, + stopOnTerminate: false, + startOnBoot: true, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.ANY, + ), + (String taskId) async { + await bgUpdateCheck(taskId, null); + BackgroundFetch.finish(taskId); + }, + (String taskId) async { + context.read().add('BG update task timed out.'); + BackgroundFetch.finish(taskId); + }, + ); if (!mounted) return; } @@ -183,30 +196,33 @@ class _ObtainiumState extends State { // If this is the first run, ask for notification permissions and add Obtainium to the Apps list Permission.notification.request(); if (!fdroid) { - getInstalledInfo(obtainiumId).then((value) { - if (value?.versionName != null) { - appsProvider.saveApps([ - App( - obtainiumId, - obtainiumUrl, - 'ImranR98', - 'Obtainium', - value!.versionName, - value.versionName!, - [], - 0, - { - 'versionDetection': true, - 'apkFilterRegEx': 'fdroid', - 'invertAPKFilter': true - }, - null, - false) - ], onlyIfExists: false); - } - }).catchError((err) { - print(err); - }); + getInstalledInfo(obtainiumId) + .then((value) { + if (value?.versionName != null) { + appsProvider.saveApps([ + App( + obtainiumId, + obtainiumUrl, + 'ImranR98', + 'Obtainium', + value!.versionName, + value.versionName!, + [], + 0, + { + 'versionDetection': true, + 'apkFilterRegEx': 'fdroid', + 'invertAPKFilter': true, + }, + null, + false, + ), + ], onlyIfExists: false); + } + }) + .catchError((err) { + print(err); + }); } } if (!supportedLocales.map((e) => e.key).contains(context.locale) || @@ -221,32 +237,35 @@ class _ObtainiumState extends State { }); return DynamicColorBuilder( - builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { - // Decide on a colour/brightness scheme based on OS and user settings - ColorScheme lightColorScheme; - ColorScheme darkColorScheme; - if (lightDynamic != null && - darkDynamic != null && - settingsProvider.useMaterialYou) { - lightColorScheme = lightDynamic.harmonized(); - darkColorScheme = darkDynamic.harmonized(); - } else { - lightColorScheme = - ColorScheme.fromSeed(seedColor: settingsProvider.themeColor); - darkColorScheme = ColorScheme.fromSeed( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + // Decide on a colour/brightness scheme based on OS and user settings + ColorScheme lightColorScheme; + ColorScheme darkColorScheme; + if (lightDynamic != null && + darkDynamic != null && + settingsProvider.useMaterialYou) { + lightColorScheme = lightDynamic.harmonized(); + darkColorScheme = darkDynamic.harmonized(); + } else { + lightColorScheme = ColorScheme.fromSeed( seedColor: settingsProvider.themeColor, - brightness: Brightness.dark); - } + ); + darkColorScheme = ColorScheme.fromSeed( + seedColor: settingsProvider.themeColor, + brightness: Brightness.dark, + ); + } - // set the background and surface colors to pure black in the amoled theme - if (settingsProvider.useBlackTheme) { - darkColorScheme = - darkColorScheme.copyWith(surface: Colors.black).harmonized(); - } + // set the background and surface colors to pure black in the amoled theme + if (settingsProvider.useBlackTheme) { + darkColorScheme = darkColorScheme + .copyWith(surface: Colors.black) + .harmonized(); + } - if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont(); + if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont(); - return MaterialApp( + return MaterialApp( title: 'Obtainium', localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, @@ -254,22 +273,31 @@ class _ObtainiumState extends State { navigatorKey: globalNavigatorKey, debugShowCheckedModeBanner: false, theme: ThemeData( - useMaterial3: true, - colorScheme: settingsProvider.theme == ThemeSettings.dark - ? darkColorScheme - : lightColorScheme, - fontFamily: - settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'), + useMaterial3: true, + colorScheme: settingsProvider.theme == ThemeSettings.dark + ? darkColorScheme + : lightColorScheme, + fontFamily: settingsProvider.useSystemFont + ? 'SystemFont' + : 'Montserrat', + ), darkTheme: ThemeData( - useMaterial3: true, - colorScheme: settingsProvider.theme == ThemeSettings.light - ? lightColorScheme - : darkColorScheme, - fontFamily: - settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'), - home: Shortcuts(shortcuts: { - LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), - }, child: const HomePage())); - }); + useMaterial3: true, + colorScheme: settingsProvider.theme == ThemeSettings.light + ? lightColorScheme + : darkColorScheme, + fontFamily: settingsProvider.useSystemFont + ? 'SystemFont' + : 'Montserrat', + ), + home: Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), + }, + child: const HomePage(), + ), + ); + }, + ); } } diff --git a/lib/main_fdroid.dart b/lib/main_fdroid.dart index 7fb8a28..93c0f66 100644 --- a/lib/main_fdroid.dart +++ b/lib/main_fdroid.dart @@ -3,4 +3,4 @@ import 'main.dart' as m; void main() async { m.fdroid = true; m.main(); -} \ No newline at end of file +} diff --git a/lib/mass_app_sources/githubstars.dart b/lib/mass_app_sources/githubstars.dart index 4a7ac61..94647bd 100644 --- a/lib/mass_app_sources/githubstars.dart +++ b/lib/mass_app_sources/githubstars.dart @@ -14,11 +14,15 @@ class GitHubStars implements MassAppUrlSource { late List requiredArgs = [tr('uname')]; Future>> getOnePageOfUserStarredUrlsWithDescriptions( - String username, int page) async { + String username, + int page, + ) async { Response res = await get( - Uri.parse( - 'https://api.github.com/users/$username/starred?per_page=100&page=$page'), - headers: await GitHub().getRequestHeaders({})); + Uri.parse( + 'https://api.github.com/users/$username/starred?per_page=100&page=$page', + ), + headers: await GitHub().getRequestHeaders({}), + ); if (res.statusCode == 200) { Map> urlsWithDescriptions = {}; for (var e in (jsonDecode(res.body) as List)) { @@ -27,8 +31,8 @@ class GitHubStars implements MassAppUrlSource { e['full_name'] as String, e['description'] != null ? e['description'] as String - : tr('noDescription') - ] + : tr('noDescription'), + ], }); } return urlsWithDescriptions; @@ -41,15 +45,18 @@ class GitHubStars implements MassAppUrlSource { @override Future>> getUrlsWithDescriptions( - List args) async { + List args, + ) async { if (args.length != requiredArgs.length) { throw ObtainiumError(tr('wrongArgNum')); } Map> urlsWithDescriptions = {}; var page = 1; while (true) { - var pageUrls = - await getOnePageOfUserStarredUrlsWithDescriptions(args[0], page++); + var pageUrls = await getOnePageOfUserStarredUrlsWithDescriptions( + args[0], + page++, + ); urlsWithDescriptions.addAll(pageUrls); if (pageUrls.length < 100) { break; diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 327819a..6251737 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -51,8 +51,13 @@ class AddAppPageState extends State { } } - changeUserInput(String input, bool valid, bool isBuilding, - {bool updateUrlInput = false, String? overrideSource}) { + changeUserInput( + String input, + bool valid, + bool isBuilding, { + bool updateUrlInput = false, + String? overrideSource, + }) { userInput = input; if (!isBuilding) { setState(() { @@ -69,8 +74,10 @@ class AddAppPageState extends State { ? pickedSource?.hosts[0] : null; var source = valid - ? sourceProvider.getSource(userInput, - overrideSource: pickedSourceOverride) + ? sourceProvider.getSource( + userInput, + overrideSource: pickedSourceOverride, + ) : null; if (pickedSource.runtimeType != source.runtimeType || overrideChanged || @@ -79,7 +86,8 @@ class AddAppPageState extends State { pickedSource?.runOnAddAppInputChange(userInput); additionalSettings = source != null ? getDefaultValuesFromFormItems( - source.combinedAppSpecificSettingFormItems) + source.combinedAppSpecificSettingFormItems, + ) : {}; additionalSettingsValid = source != null ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) @@ -94,32 +102,38 @@ class AddAppPageState extends State { Widget build(BuildContext context) { AppsProvider appsProvider = context.read(); SettingsProvider settingsProvider = context.watch(); - NotificationsProvider notificationsProvider = - context.read(); + NotificationsProvider notificationsProvider = context + .read(); bool doingSomething = gettingAppInfo || searching; - Future getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly, - {bool ignoreHideSetting = false}) async { + Future getTrackOnlyConfirmationIfNeeded( + bool userPickedTrackOnly, { + bool ignoreHideSetting = false, + }) async { var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly; if (useTrackOnly && (!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) { // ignore: use_build_context_synchronously var values = await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - initValid: true, - title: tr('xIsTrackOnly', args: [ - pickedSource!.enforceTrackOnly ? tr('source') : tr('app') - ]), - items: [ - [GeneratedFormSwitch('hide', label: tr('dontShowAgain'))] + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + initValid: true, + title: tr( + 'xIsTrackOnly', + args: [ + pickedSource!.enforceTrackOnly ? tr('source') : tr('app'), ], - message: - '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', - ); - }); + ), + items: [ + [GeneratedFormSwitch('hide', label: tr('dontShowAgain'))], + ], + message: + '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', + ); + }, + ); if (values != null) { settingsProvider.hideTrackOnlyWarning = values['hide'] == true; } @@ -130,18 +144,20 @@ class AddAppPageState extends State { } getReleaseDateAsVersionConfirmationIfNeeded( - bool userPickedTrackOnly) async { + bool userPickedTrackOnly, + ) async { return (!(additionalSettings['releaseDateAsVersion'] == true && // ignore: use_build_context_synchronously await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('releaseDateAsVersion'), - items: const [], - message: tr('releaseDateAsVersionExplanation'), - ); - }) == + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('releaseDateAsVersion'), + items: const [], + message: tr('releaseDateAsVersionExplanation'), + ); + }, + ) == null)); } @@ -154,27 +170,38 @@ class AddAppPageState extends State { App? app; if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) && (await getReleaseDateAsVersionConfirmationIfNeeded( - userPickedTrackOnly))) { + userPickedTrackOnly, + ))) { var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; app = await sourceProvider.getApp( - pickedSource!, userInput.trim(), additionalSettings, - trackOnlyOverride: trackOnly, - sourceIsOverriden: pickedSourceOverride != null, - inferAppIdIfOptional: inferAppIdIfOptional); + pickedSource!, + userInput.trim(), + additionalSettings, + trackOnlyOverride: trackOnly, + sourceIsOverriden: pickedSourceOverride != null, + inferAppIdIfOptional: inferAppIdIfOptional, + ); // Only download the APK here if you need to for the package ID if (isTempId(app) && app.additionalSettings['trackOnly'] != true) { // ignore: use_build_context_synchronously - var apkUrl = - await appsProvider.confirmAppFileUrl(app, context, false); + var apkUrl = await appsProvider.confirmAppFileUrl( + app, + context, + false, + ); if (apkUrl == null) { throw ObtainiumError(tr('cancelled')); } - app.preferredApkIndex = - app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); + app.preferredApkIndex = app.apkUrls + .map((e) => e.value) + .toList() + .indexOf(apkUrl.value); // ignore: use_build_context_synchronously var downloadedArtifact = await appsProvider.downloadApp( - app, globalNavigatorKey.currentContext, - notificationsProvider: notificationsProvider); + app, + globalNavigatorKey.currentContext, + notificationsProvider: notificationsProvider, + ); DownloadedApk? downloadedFile; DownloadedXApkDir? downloadedDir; if (downloadedArtifact is DownloadedApk) { @@ -195,8 +222,10 @@ class AddAppPageState extends State { await appsProvider.saveApps([app], onlyIfExists: false); } if (app != null) { - Navigator.push(globalNavigatorKey.currentContext ?? context, - MaterialPageRoute(builder: (context) => AppPage(appId: app!.id))); + Navigator.push( + globalNavigatorKey.currentContext ?? context, + MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)), + ); } } catch (e) { showError(e, context); @@ -211,57 +240,63 @@ class AddAppPageState extends State { } Widget getUrlInputRow() => Row( - children: [ - Expanded( - child: GeneratedForm( - key: Key(urlInputKey.toString()), - items: [ - [ - GeneratedFormTextField('appSourceURL', - label: tr('appSourceURL'), - defaultValue: userInput, - additionalValidators: [ - (value) { - try { - sourceProvider - .getSource(value ?? '', - overrideSource: pickedSourceOverride) - .standardizeUrl(value ?? ''); - } catch (e) { - return e is String - ? e - : e is ObtainiumError - ? e.toString() - : tr('error'); - } - return null; - } - ]) - ] - ], - onValueChanges: (values, valid, isBuilding) { - changeUserInput( - values['appSourceURL']!, valid, isBuilding); - })), - const SizedBox( - width: 16, - ), - gettingAppInfo - ? const CircularProgressIndicator() - : ElevatedButton( - onPressed: doingSomething || - pickedSource == null || - (pickedSource!.combinedAppSpecificSettingFormItems - .isNotEmpty && - !additionalSettingsValid) - ? null - : () { - HapticFeedback.selectionClick(); - addApp(); - }, - child: Text(tr('add'))) - ], - ); + children: [ + Expanded( + child: GeneratedForm( + key: Key(urlInputKey.toString()), + items: [ + [ + GeneratedFormTextField( + 'appSourceURL', + label: tr('appSourceURL'), + defaultValue: userInput, + additionalValidators: [ + (value) { + try { + sourceProvider + .getSource( + value ?? '', + overrideSource: pickedSourceOverride, + ) + .standardizeUrl(value ?? ''); + } catch (e) { + return e is String + ? e + : e is ObtainiumError + ? e.toString() + : tr('error'); + } + return null; + }, + ], + ), + ], + ], + onValueChanges: (values, valid, isBuilding) { + changeUserInput(values['appSourceURL']!, valid, isBuilding); + }, + ), + ), + const SizedBox(width: 16), + gettingAppInfo + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: + doingSomething || + pickedSource == null || + (pickedSource! + .combinedAppSpecificSettingFormItems + .isNotEmpty && + !additionalSettingsValid) + ? null + : () { + HapticFeedback.selectionClick(); + addApp(); + }, + child: Text(tr('add')), + ), + ], + ); runSearch({bool filtered = true}) async { setState(() { @@ -272,82 +307,94 @@ class AddAppPageState extends State { sourceStrings[s.name] = [s.name]; }); try { - var searchSources = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return SelectionModal( - title: tr('selectX', args: [plural('source', 2)]), - entries: sourceStrings, - selectedByDefault: true, - onlyOneSelectionAllowed: false, - titlesAreLinks: false, - deselectThese: settingsProvider.searchDeselected, - ); - }) ?? + var searchSources = + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return SelectionModal( + title: tr('selectX', args: [plural('source', 2)]), + entries: sourceStrings, + selectedByDefault: true, + onlyOneSelectionAllowed: false, + titlesAreLinks: false, + deselectThese: settingsProvider.searchDeselected, + ); + }, + ) ?? []; if (searchSources.isNotEmpty) { settingsProvider.searchDeselected = sourceStrings.keys .where((s) => !searchSources.contains(s)) .toList(); - List>>?> results = - (await Future.wait(sourceProvider.sources - .where((e) => searchSources.contains(e.name)) - .map((e) async { - try { - Map? querySettings = {}; - if (e.includeAdditionalOptsInMainSearch) { - querySettings = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('searchX', args: [e.name]), - items: [ - ...e.searchQuerySettingFormItems.map((e) => [e]), - [ - GeneratedFormTextField('url', - label: e.hosts.isNotEmpty - ? tr('overrideSource') - : plural('url', 1).substring(2), - autoCompleteOptions: [ - ...(e.hosts.isNotEmpty ? [e.hosts[0]] : []), - ...appsProvider.apps.values - .where((a) => - sourceProvider - .getSource(a.app.url, - overrideSource: - a.app.overrideSource) - .runtimeType == - e.runtimeType) - .map((a) { - var uri = Uri.parse(a.app.url); - return '${uri.origin}${uri.path}'; - }) - ], - defaultValue: - e.hosts.isNotEmpty ? e.hosts[0] : '', - required: true) - ], - ], + List>>?> + results = (await Future.wait( + sourceProvider.sources + .where((e) => searchSources.contains(e.name)) + .map((e) async { + try { + Map? querySettings = {}; + if (e.includeAdditionalOptsInMainSearch) { + querySettings = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('searchX', args: [e.name]), + items: [ + ...e.searchQuerySettingFormItems.map((e) => [e]), + [ + GeneratedFormTextField( + 'url', + label: e.hosts.isNotEmpty + ? tr('overrideSource') + : plural('url', 1).substring(2), + autoCompleteOptions: [ + ...(e.hosts.isNotEmpty ? [e.hosts[0]] : []), + ...appsProvider.apps.values + .where( + (a) => + sourceProvider + .getSource( + a.app.url, + overrideSource: + a.app.overrideSource, + ) + .runtimeType == + e.runtimeType, + ) + .map((a) { + var uri = Uri.parse(a.app.url); + return '${uri.origin}${uri.path}'; + }), + ], + defaultValue: e.hosts.isNotEmpty + ? e.hosts[0] + : '', + required: true, + ), + ], + ], + ); + }, ); - }); - if (querySettings == null) { - return null; - } - } - return MapEntry(e.runtimeType.toString(), - await e.search(searchQuery, querySettings: querySettings)); - } catch (err) { - if (err is! CredsNeededError) { - rethrow; - } else { - err.unexpected = true; - showError(err, context); - return null; - } - } - }))) - .where((a) => a != null) - .toList(); + if (querySettings == null) { + return null; + } + } + return MapEntry( + e.runtimeType.toString(), + await e.search(searchQuery, querySettings: querySettings), + ); + } catch (err) { + if (err is! CredsNeededError) { + rethrow; + } else { + err.unexpected = true; + showError(err, context); + return null; + } + } + }), + )).where((a) => a != null).toList(); // Interleave results instead of simple reduce Map>> res = {}; @@ -379,11 +426,17 @@ class AddAppPageState extends State { selectedByDefault: false, onlyOneSelectionAllowed: true, ); - }); + }, + ); if (selectedUrls != null && selectedUrls.isNotEmpty) { var sourceName = res[selectedUrls[0]]?.key; - changeUserInput(selectedUrls[0], true, false, - updateUrlInput: true, overrideSource: sourceName); + changeUserInput( + selectedUrls[0], + true, + false, + updateUrlInput: true, + overrideSource: sourceName, + ); } } } catch (e) { @@ -395,33 +448,39 @@ class AddAppPageState extends State { } } - Widget getHTMLSourceOverrideDropdown() => Column(children: [ - Row( - children: [ - Expanded( - child: GeneratedForm( + Widget getHTMLSourceOverrideDropdown() => Column( + children: [ + Row( + children: [ + Expanded( + child: GeneratedForm( items: [ [ GeneratedFormDropdown( - 'overrideSource', - defaultValue: pickedSourceOverride ?? '', - [ - MapEntry('', tr('none')), - ...sourceProvider.sources - .where((s) => + 'overrideSource', + defaultValue: pickedSourceOverride ?? '', + [ + MapEntry('', tr('none')), + ...sourceProvider.sources + .where( + (s) => s.allowOverride || (pickedSource != null && pickedSource.runtimeType == - s.runtimeType)) - .map((s) => - MapEntry(s.runtimeType.toString(), s.name)) - ], - label: tr('overrideSource')) - ] + s.runtimeType), + ) + .map( + (s) => MapEntry(s.runtimeType.toString(), s.name), + ), + ], + label: tr('overrideSource'), + ), + ], ], onValueChanges: (values, valid, isBuilding) { fn() { - pickedSourceOverride = (values['overrideSource'] == null || + pickedSourceOverride = + (values['overrideSource'] == null || values['overrideSource'] == '') ? null : values['overrideSource']; @@ -436,13 +495,13 @@ class AddAppPageState extends State { } changeUserInput(userInput, valid, isBuilding); }, - )) - ], - ), - const SizedBox( - height: 16, - ) - ]); + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ); bool shouldShowSearchBar() => sourceProvider.sources.where((e) => e.canSearch).isNotEmpty && @@ -450,249 +509,260 @@ class AddAppPageState extends State { userInput.isEmpty; Widget getSearchBarRow() => Row( - children: [ - Expanded( - child: GeneratedForm( - items: [ - [ - GeneratedFormTextField('searchSomeSources', - label: tr('searchSomeSourcesLabel'), required: false), - ] - ], - onValueChanges: (values, valid, isBuilding) { - if (values.isNotEmpty && valid && !isBuilding) { - setState(() { - searchQuery = values['searchSomeSources']!.trim(); - }); - } - }), - ), - const SizedBox( - width: 16, - ), - searching - ? const CircularProgressIndicator() - : ElevatedButton( - onPressed: searchQuery.isEmpty || doingSomething - ? null - : () { - runSearch(); - }, - child: Text(tr('search'))) - ], - ); + children: [ + Expanded( + child: GeneratedForm( + items: [ + [ + GeneratedFormTextField( + 'searchSomeSources', + label: tr('searchSomeSourcesLabel'), + required: false, + ), + ], + ], + onValueChanges: (values, valid, isBuilding) { + if (values.isNotEmpty && valid && !isBuilding) { + setState(() { + searchQuery = values['searchSomeSources']!.trim(); + }); + } + }, + ), + ), + const SizedBox(width: 16), + searching + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: searchQuery.isEmpty || doingSomething + ? null + : () { + runSearch(); + }, + child: Text(tr('search')), + ), + ], + ); Widget getAdditionalOptsCol() => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 16, - ), - Text( - tr('additionalOptsFor', - args: [pickedSource?.name ?? tr('source')]), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold)), - const SizedBox( - height: 16, - ), - GeneratedForm( - key: Key( - '${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}'), - items: [ - ...pickedSource!.combinedAppSpecificSettingFormItems, - ...(pickedSourceOverride != null - ? pickedSource!.sourceConfigSettingFormItems - .map((e) => [e]) - : []) - ], - onValueChanges: (values, valid, isBuilding) { - if (!isBuilding) { - setState(() { - additionalSettings = values; - additionalSettingsValid = valid; - }); - } - }), - Column( - children: [ - const SizedBox( - height: 16, - ), - CategoryEditorSelector( - alignment: WrapAlignment.start, - onSelected: (categories) { - pickedCategories = categories; - }), - ], - ), - if (pickedSource != null && pickedSource!.appIdInferIsOptional) - GeneratedForm( - key: const Key('inferAppIdIfOptional'), - items: [ - [ - GeneratedFormSwitch('inferAppIdIfOptional', - label: tr('tryInferAppIdFromCode'), - defaultValue: inferAppIdIfOptional) - ] - ], - onValueChanges: (values, valid, isBuilding) { - if (!isBuilding) { - setState(() { - inferAppIdIfOptional = values['inferAppIdIfOptional']; - }); - } - }), - if (pickedSource != null && pickedSource!.enforceTrackOnly) - GeneratedForm( - key: Key( - '${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}-appId'), - items: [ - [ - GeneratedFormTextField('appId', - label: '${tr('appId')} - ${tr('custom')}', - required: false, - additionalValidators: [ - (value) { - if (value == null || value.isEmpty) { - return null; - } - final isValid = RegExp( - r'^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Za-z][A-Za-z\d_]*$') - .hasMatch(value); - if (!isValid) { - return tr('invalidInput'); - } - return null; - } - ]), - ] - ], - onValueChanges: (values, valid, isBuilding) { - if (!isBuilding) { - setState(() { - additionalSettings['appId'] = values['appId']; - }); - } - }), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + Text( + tr('additionalOptsFor', args: [pickedSource?.name ?? tr('source')]), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + GeneratedForm( + key: Key( + '${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}', + ), + items: [ + ...pickedSource!.combinedAppSpecificSettingFormItems, + ...(pickedSourceOverride != null + ? pickedSource!.sourceConfigSettingFormItems.map((e) => [e]) + : []), ], - ); + onValueChanges: (values, valid, isBuilding) { + if (!isBuilding) { + setState(() { + additionalSettings = values; + additionalSettingsValid = valid; + }); + } + }, + ), + Column( + children: [ + const SizedBox(height: 16), + CategoryEditorSelector( + alignment: WrapAlignment.start, + onSelected: (categories) { + pickedCategories = categories; + }, + ), + ], + ), + if (pickedSource != null && pickedSource!.appIdInferIsOptional) + GeneratedForm( + key: const Key('inferAppIdIfOptional'), + items: [ + [ + GeneratedFormSwitch( + 'inferAppIdIfOptional', + label: tr('tryInferAppIdFromCode'), + defaultValue: inferAppIdIfOptional, + ), + ], + ], + onValueChanges: (values, valid, isBuilding) { + if (!isBuilding) { + setState(() { + inferAppIdIfOptional = values['inferAppIdIfOptional']; + }); + } + }, + ), + if (pickedSource != null && pickedSource!.enforceTrackOnly) + GeneratedForm( + key: Key( + '${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}-appId', + ), + items: [ + [ + GeneratedFormTextField( + 'appId', + label: '${tr('appId')} - ${tr('custom')}', + required: false, + additionalValidators: [ + (value) { + if (value == null || value.isEmpty) { + return null; + } + final isValid = RegExp( + r'^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Za-z][A-Za-z\d_]*$', + ).hasMatch(value); + if (!isValid) { + return tr('invalidInput'); + } + return null; + }, + ], + ), + ], + ], + onValueChanges: (values, valid, isBuilding) { + if (!isBuilding) { + setState(() { + additionalSettings['appId'] = values['appId']; + }); + } + }, + ), + ], + ); Widget getSourcesListWidget() => Padding( - padding: const EdgeInsets.all(16), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.spaceBetween, - spacing: 12, - children: [ - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - return GeneratedFormModal( - singleNullReturnButton: tr('ok'), - title: tr('supportedSources'), - items: const [], - additionalWidgets: [ - ...sourceProvider.sources.map( - (e) => Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: e.hosts.isNotEmpty - ? () { - launchUrlString( - 'https://${e.hosts[0]}', - mode: LaunchMode - .externalApplication); - } - : null, - child: Text( - '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', - style: TextStyle( - decoration: e.hosts.isNotEmpty - ? TextDecoration.underline - : TextDecoration.none), - ))), + padding: const EdgeInsets.all(16), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + spacing: 12, + children: [ + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) { + return GeneratedFormModal( + singleNullReturnButton: tr('ok'), + title: tr('supportedSources'), + items: const [], + additionalWidgets: [ + ...sourceProvider.sources.map( + (e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: e.hosts.isNotEmpty + ? () { + launchUrlString( + 'https://${e.hosts[0]}', + mode: LaunchMode.externalApplication, + ); + } + : null, + child: Text( + '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', + style: TextStyle( + decoration: e.hosts.isNotEmpty + ? TextDecoration.underline + : TextDecoration.none, + ), ), - const SizedBox( - height: 16, - ), - Text( - '${tr('note')}:', - style: - const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox( - height: 4, - ), - Text(tr('selfHostedNote', - args: [tr('overrideSource')])), - ], - ); - }, - ); - }, - child: Text( - tr('supportedSources'), - style: const TextStyle( - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic), - )), - GestureDetector( - onTap: () { - launchUrlString('https://apps.obtainium.imranr.dev/', - mode: LaunchMode.externalApplication); + ), + ), + ), + const SizedBox(height: 16), + Text( + '${tr('note')}:', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text(tr('selfHostedNote', args: [tr('overrideSource')])), + ], + ); }, - child: Text( - tr('crowdsourcedConfigsShort'), - style: const TextStyle( - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic), - ), + ); + }, + child: Text( + tr('supportedSources'), + style: const TextStyle( + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, ), - ], + ), ), - ); + GestureDetector( + onTap: () { + launchUrlString( + 'https://apps.obtainium.imranr.dev/', + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + tr('crowdsourcedConfigsShort'), + style: const TextStyle( + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - bottomNavigationBar: - pickedSource == null ? getSourcesListWidget() : null, - body: CustomScrollView(shrinkWrap: true, slivers: [ + backgroundColor: Theme.of(context).colorScheme.surface, + bottomNavigationBar: pickedSource == null ? getSourcesListWidget() : null, + body: CustomScrollView( + shrinkWrap: true, + slivers: [ CustomAppBar(title: tr('addApp')), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - getUrlInputRow(), - const SizedBox( - height: 16, - ), - if (pickedSource != null) getHTMLSourceOverrideDropdown(), - if (shouldShowSearchBar()) getSearchBarRow(), - if (pickedSource != null) - FutureBuilder( - builder: (ctx, val) { - return val.data != null && val.data!.isNotEmpty - ? Text( - val.data!, - style: - Theme.of(context).textTheme.bodySmall, - ) - : const SizedBox(); - }, - future: pickedSource?.getSourceNote()), - if (pickedSource != null) getAdditionalOptsCol(), - ])), - ) - ])); + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + getUrlInputRow(), + const SizedBox(height: 16), + if (pickedSource != null) getHTMLSourceOverrideDropdown(), + if (shouldShowSearchBar()) getSearchBarRow(), + if (pickedSource != null) + FutureBuilder( + builder: (ctx, val) { + return val.data != null && val.data!.isNotEmpty + ? Text( + val.data!, + style: Theme.of(context).textTheme.bodySmall, + ) + : const SizedBox(); + }, + future: pickedSource?.getSourceNote(), + ), + if (pickedSource != null) getAdditionalOptsCol(), + ], + ), + ), + ), + ], + ), + ); } } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 1c856b8..27068ce 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -40,13 +40,15 @@ class _AppPageState extends State { onWebResourceError: (WebResourceError error) { if (error.isForMainFrame == true) { showError( - ObtainiumError(error.description, unexpected: true), context); + ObtainiumError(error.description, unexpected: true), + context, + ); } }, onNavigationRequest: (NavigationRequest request) => request.url.startsWith("rustore://") - ? NavigationDecision.prevent - : NavigationDecision.navigate, + ? NavigationDecision.prevent + : NavigationDecision.navigate, ), ); } @@ -85,8 +87,10 @@ class _AppPageState extends State { var sourceProvider = SourceProvider(); AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy(); var source = app != null - ? sourceProvider.getSource(app.app.url, - overrideSource: app.app.overrideSource) + ? sourceProvider.getSource( + app.app.url, + overrideSource: app.app.overrideSource, + ) : null; if (!areDownloadsRunning && prevApp == null && @@ -100,7 +104,9 @@ class _AppPageState extends State { bool isVersionDetectionStandard = app?.app.additionalSettings['versionDetection'] == true; - bool installedVersionIsEstimate = app?.app != null ? isVersionPseudo(app!.app) : false; + bool installedVersionIsEstimate = app?.app != null + ? isVersionPseudo(app!.app) + : false; if (app != null && !_wasWebViewOpened) { _wasWebViewOpened = true; @@ -122,11 +128,14 @@ class _AppPageState extends State { if (!upToDate) { versionLines += '\n${app?.app.latestVersion} ${tr('latest')}'; } - String infoLines = tr('lastUpdateCheckX', args: [ - app?.app.lastUpdateCheck == null - ? tr('never') - : '${app?.app.lastUpdateCheck?.toLocal()}' - ]); + String infoLines = tr( + 'lastUpdateCheckX', + args: [ + app?.app.lastUpdateCheck == null + ? tr('never') + : '${app?.app.lastUpdateCheck?.toLocal()}', + ], + ); if (trackOnly) { infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines'; } @@ -146,15 +155,14 @@ class _AppPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24), child: Column( children: [ - const SizedBox( - height: 8, + const SizedBox(height: 8), + Text( + versionLines, + textAlign: TextAlign.start, + style: Theme.of( + context, + ).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold), ), - Text(versionLines, - textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(fontWeight: FontWeight.bold)), changeLogFn != null || app?.app.releaseDate != null ? GestureDetector( onTap: changeLogFn, @@ -163,21 +171,19 @@ class _AppPageState extends State { ? tr('changes') : app!.app.releaseDate!.toLocal().toString(), textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.labelSmall!.copyWith( - decoration: changeLogFn != null - ? TextDecoration.underline - : null, - fontStyle: changeLogFn != null - ? FontStyle.italic - : null, - ), + style: Theme.of(context).textTheme.labelSmall! + .copyWith( + decoration: changeLogFn != null + ? TextDecoration.underline + : null, + fontStyle: changeLogFn != null + ? FontStyle.italic + : null, + ), ), ) : const SizedBox.shrink(), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ], ), ), @@ -189,101 +195,108 @@ class _AppPageState extends State { if (app?.app.apkUrls.isNotEmpty == true || app?.app.otherAssetUrls.isNotEmpty == true) GestureDetector( - onTap: app?.app == null || updating - ? null - : () async { - try { - await appsProvider - .downloadAppAssets([app!.app.id], context); - } catch (e) { - showError(e, context); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: settingsProvider.highlightTouchTargets - ? (Theme.of(context).brightness == - Brightness.light - ? Theme.of(context).primaryColor - : Theme.of(context).primaryColorLight) - .withAlpha(Theme.of(context).brightness == - Brightness.light - ? 20 - : 40) - : null), - padding: settingsProvider.highlightTouchTargets - ? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6) - : const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6), - margin: - const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0), - child: Text( - tr('downloadX', - args: [tr('releaseAsset').toLowerCase()]), - textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.labelSmall!.copyWith( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic, - ), - )) - ], - )), - const SizedBox( - height: 48, - ), + onTap: app?.app == null || updating + ? null + : () async { + try { + await appsProvider.downloadAppAssets([ + app!.app.id, + ], context); + } catch (e) { + showError(e, context); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: settingsProvider.highlightTouchTargets + ? (Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColorLight) + .withAlpha( + Theme.of(context).brightness == + Brightness.light + ? 20 + : 40, + ) + : null, + ), + padding: settingsProvider.highlightTouchTargets + ? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6) + : const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6), + margin: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0), + child: Text( + tr('downloadX', args: [tr('releaseAsset').toLowerCase()]), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 48), CategoryEditorSelector( - alignment: WrapAlignment.center, - preselected: app?.app.categories != null - ? app!.app.categories.toSet() - : {}, - onSelected: (categories) { - if (app != null) { - app.app.categories = categories; - appsProvider.saveApps([app.app]); - } - }), + alignment: WrapAlignment.center, + preselected: app?.app.categories != null + ? app!.app.categories.toSet() + : {}, + onSelected: (categories) { + if (app != null) { + app.app.categories = categories; + appsProvider.saveApps([app.app]); + } + }, + ), if (app?.app.additionalSettings['about'] is String && app?.app.additionalSettings['about'].isNotEmpty) Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 48, - ), + const SizedBox(height: 48), GestureDetector( - onLongPress: () { - Clipboard.setData(ClipboardData( - text: app?.app.additionalSettings['about'] ?? '')); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(tr('copiedToClipboard')), - )); - }, - child: Markdown( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - styleSheet: MarkdownStyleSheet( - blockquoteDecoration: - BoxDecoration(color: Theme.of(context).cardColor), - textAlign: WrapAlignment.center), - data: app?.app.additionalSettings['about'], - onTapLink: (text, href, title) { - if (href != null) { - launchUrlString(href, - mode: LaunchMode.externalApplication); - } - }, - extensionSet: md.ExtensionSet( - md.ExtensionSet.gitHubFlavored.blockSyntaxes, - [ - md.EmojiSyntax(), - ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes - ], + onLongPress: () { + Clipboard.setData( + ClipboardData( + text: app?.app.additionalSettings['about'] ?? '', ), - )) + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tr('copiedToClipboard'))), + ); + }, + child: Markdown( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + styleSheet: MarkdownStyleSheet( + blockquoteDecoration: BoxDecoration( + color: Theme.of(context).cardColor, + ), + textAlign: WrapAlignment.center, + ), + data: app?.app.additionalSettings['about'], + onTapLink: (text, href, title) { + if (href != null) { + launchUrlString( + href, + mode: LaunchMode.externalApplication, + ); + } + }, + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [ + md.EmojiSyntax(), + ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes, + ], + ), + ), + ), ], ), ], @@ -291,132 +304,143 @@ class _AppPageState extends State { } getFullInfoColumn({bool small = false}) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: small ? 5 : 20), - FutureBuilder( - future: - appsProvider.updateAppIcon(app?.app.id, ignoreCache: true), - builder: (ctx, val) { - return app?.icon != null - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: app == null - ? null - : () => pm.openApp(app.app.id), - child: Image.memory( - app!.icon!, - height: small ? 70 : 150, - gaplessPlayback: true, - ), - ) - ]) - : Container(); - }), - SizedBox( - height: small ? 10 : 25, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: small ? 5 : 20), + FutureBuilder( + future: appsProvider.updateAppIcon(app?.app.id, ignoreCache: true), + builder: (ctx, val) { + return app?.icon != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: app == null + ? null + : () => pm.openApp(app.app.id), + child: Image.memory( + app!.icon!, + height: small ? 70 : 150, + gaplessPlayback: true, + ), + ), + ], + ) + : Container(); + }, + ), + SizedBox(height: small ? 10 : 25), + Text( + app?.name ?? tr('app'), + textAlign: TextAlign.center, + style: small + ? Theme.of(context).textTheme.displaySmall + : Theme.of(context).textTheme.displayLarge, + ), + Text( + tr('byX', args: [app?.author ?? tr('unknown')]), + textAlign: TextAlign.center, + style: small + ? Theme.of(context).textTheme.headlineSmall + : Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 24), + GestureDetector( + onTap: () { + if (app?.app.url != null) { + launchUrlString( + app?.app.url ?? '', + mode: LaunchMode.externalApplication, + ); + } + }, + onLongPress: () { + Clipboard.setData(ClipboardData(text: app?.app.url ?? '')); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard')))); + }, + child: Text( + app?.app.url ?? '', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, ), - Text( - app?.name ?? tr('app'), - textAlign: TextAlign.center, - style: small - ? Theme.of(context).textTheme.displaySmall - : Theme.of(context).textTheme.displayLarge, - ), - Text(tr('byX', args: [app?.author ?? tr('unknown')]), - textAlign: TextAlign.center, - style: small - ? Theme.of(context).textTheme.headlineSmall - : Theme.of(context).textTheme.headlineMedium), - const SizedBox( - height: 24, - ), - GestureDetector( - onTap: () { - if (app?.app.url != null) { - launchUrlString(app?.app.url ?? '', - mode: LaunchMode.externalApplication); - } - }, - onLongPress: () { - Clipboard.setData(ClipboardData(text: app?.app.url ?? '')); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(tr('copiedToClipboard')), - )); - }, - child: Text( - app?.app.url ?? '', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall!.copyWith( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic), - )), - Text( - app?.app.id ?? '', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall, - ), - getInfoColumn(), - const SizedBox(height: 150) - ], - ); + ), + ), + Text( + app?.app.id ?? '', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall, + ), + getInfoColumn(), + const SizedBox(height: 150), + ], + ); getAppWebView() => app != null ? WebViewWidget( key: ObjectKey(_webViewController), controller: _webViewController - ..setBackgroundColor(Theme.of(context).colorScheme.surface)) + ..setBackgroundColor(Theme.of(context).colorScheme.surface), + ) : Container(); showMarkUpdatedDialog() { return showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - title: Text(tr('alreadyUpToDateQuestion')), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('no'))), - TextButton( - onPressed: () { - HapticFeedback.selectionClick(); - var updatedApp = app?.app; - if (updatedApp != null) { - updatedApp.installedVersion = updatedApp.latestVersion; - appsProvider.saveApps([updatedApp]); - } - Navigator.of(context).pop(); - }, - child: Text(tr('yesMarkUpdated'))) - ], - ); - }); + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text(tr('alreadyUpToDateQuestion')), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('no')), + ), + TextButton( + onPressed: () { + HapticFeedback.selectionClick(); + var updatedApp = app?.app; + if (updatedApp != null) { + updatedApp.installedVersion = updatedApp.latestVersion; + appsProvider.saveApps([updatedApp]); + } + Navigator.of(context).pop(); + }, + child: Text(tr('yesMarkUpdated')), + ), + ], + ); + }, + ); } showAdditionalOptionsDialog() async { return await showDialog?>( - context: context, - builder: (BuildContext ctx) { - var items = - (source?.combinedAppSpecificSettingFormItems ?? []).map((row) { - row = row.map((e) { - if (app?.app.additionalSettings[e.key] != null) { - e.defaultValue = app?.app.additionalSettings[e.key]; - } - return e; - }).toList(); - return row; + context: context, + builder: (BuildContext ctx) { + var items = (source?.combinedAppSpecificSettingFormItems ?? []).map(( + row, + ) { + row = row.map((e) { + if (app?.app.additionalSettings[e.key] != null) { + e.defaultValue = app?.app.additionalSettings[e.key]; + } + return e; }).toList(); + return row; + }).toList(); - return GeneratedFormModal( - title: tr('additionalOptions'), items: items); - }); + return GeneratedFormModal( + title: tr('additionalOptions'), + items: items, + ); + }, + ); } handleAdditionalOptionChanges(Map? values) { @@ -430,18 +454,18 @@ class _AppPageState extends State { } var versionDetectionEnabled = app.app.additionalSettings['versionDetection'] == true && - originalSettings['versionDetection'] != true; + originalSettings['versionDetection'] != true; var releaseDateVersionEnabled = app.app.additionalSettings['releaseDateAsVersion'] == true && - originalSettings['releaseDateAsVersion'] != true; + originalSettings['releaseDateAsVersion'] != true; var releaseDateVersionDisabled = app.app.additionalSettings['releaseDateAsVersion'] != true && - originalSettings['releaseDateAsVersion'] == true; + originalSettings['releaseDateAsVersion'] == true; if (releaseDateVersionEnabled) { if (app.app.releaseDate != null) { bool isUpdated = app.app.installedVersion == app.app.latestVersion; - app.app.latestVersion = - app.app.releaseDate!.microsecondsSinceEpoch.toString(); + app.app.latestVersion = app.app.releaseDate!.microsecondsSinceEpoch + .toString(); if (isUpdated) { app.app.installedVersion = app.app.latestVersion; } @@ -461,172 +485,195 @@ class _AppPageState extends State { } getInstallOrUpdateButton() => TextButton( - onPressed: !updating && - (app?.app.installedVersion == null || - app?.app.installedVersion != app?.app.latestVersion) && - !areDownloadsRunning - ? () async { - try { - var successMessage = app?.app.installedVersion == null - ? tr('installed') - : tr('appsUpdated'); - HapticFeedback.heavyImpact(); - var res = await appsProvider.downloadAndInstallLatestApps( - app?.app.id != null ? [app!.app.id] : [], - globalNavigatorKey.currentContext, - ); - if (res.isNotEmpty && !trackOnly) { - // ignore: use_build_context_synchronously - showMessage(successMessage, context); - } - if (res.isNotEmpty && mounted) { - Navigator.of(context).pop(); - } - } catch (e) { + onPressed: + !updating && + (app?.app.installedVersion == null || + app?.app.installedVersion != app?.app.latestVersion) && + !areDownloadsRunning + ? () async { + try { + var successMessage = app?.app.installedVersion == null + ? tr('installed') + : tr('appsUpdated'); + HapticFeedback.heavyImpact(); + var res = await appsProvider.downloadAndInstallLatestApps( + app?.app.id != null ? [app!.app.id] : [], + globalNavigatorKey.currentContext, + ); + if (res.isNotEmpty && !trackOnly) { // ignore: use_build_context_synchronously - showError(e, context); + showMessage(successMessage, context); } + if (res.isNotEmpty && mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + // ignore: use_build_context_synchronously + showError(e, context); } - : null, - child: Text(app?.app.installedVersion == null + } + : null, + child: Text( + app?.app.installedVersion == null ? !trackOnly - ? tr('install') - : tr('markInstalled') + ? tr('install') + : tr('markInstalled') : !trackOnly - ? tr('update') - : tr('markUpdated'))); + ? tr('update') + : tr('markUpdated'), + ), + ); getBottomSheetMenu() => Padding( - padding: - EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (source != null && - source.combinedAppSpecificSettingFormItems.isNotEmpty) - IconButton( - onPressed: app?.downloadProgress != null || updating - ? null - : () async { - var values = - await showAdditionalOptionsDialog(); - handleAdditionalOptionChanges(values); - }, - tooltip: tr('additionalOptions'), - icon: const Icon(Icons.edit)), - if (app != null && app.installedInfo != null) - IconButton( - onPressed: () { - appsProvider.openAppSettings(app.app.id); + padding: EdgeInsets.fromLTRB( + 0, + 0, + 0, + MediaQuery.of(context).padding.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (source != null && + source.combinedAppSpecificSettingFormItems.isNotEmpty) + IconButton( + onPressed: app?.downloadProgress != null || updating + ? null + : () async { + var values = await showAdditionalOptionsDialog(); + handleAdditionalOptionChanges(values); }, - icon: const Icon(Icons.settings), - tooltip: tr('settings'), - ), - if (app != null && settingsProvider.showAppWebpage) - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - content: getFullInfoColumn(small: true), - title: Text(app.name), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('continue'))) - ], - ); - }); - }, - icon: const Icon(Icons.more_horiz), - tooltip: tr('more')), - if (app?.app.installedVersion != null && - app?.app.installedVersion != app?.app.latestVersion && - !isVersionDetectionStandard && - !trackOnly) - IconButton( - onPressed: app?.downloadProgress != null || updating - ? null - : showMarkUpdatedDialog, - tooltip: tr('markUpdated'), - icon: const Icon(Icons.done)), - if ((!isVersionDetectionStandard || trackOnly) && - app?.app.installedVersion != null && - app?.app.installedVersion == app?.app.latestVersion) - IconButton( - onPressed: app?.app == null || updating - ? null - : () { - app!.app.installedVersion = null; - appsProvider.saveApps([app.app]); - }, - icon: const Icon(Icons.restore_rounded), - tooltip: tr('resetInstallStatus')), - const SizedBox(width: 16.0), - Expanded(child: getInstallOrUpdateButton()), - const SizedBox(width: 16.0), - IconButton( - onPressed: app?.downloadProgress != null || updating - ? null - : () { - appsProvider - .removeAppsWithModal( - context, app != null ? [app.app] : []) - .then((value) { - if (value == true) { - Navigator.of(context).pop(); - } - }); - }, - tooltip: tr('remove'), - icon: const Icon(Icons.delete_outline), - ), - ])), - if (app?.downloadProgress != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), - child: LinearProgressIndicator( - value: app!.downloadProgress! >= 0 - ? app.downloadProgress! / 100 - : null)) - ], - )); + tooltip: tr('additionalOptions'), + icon: const Icon(Icons.edit), + ), + if (app != null && app.installedInfo != null) + IconButton( + onPressed: () { + appsProvider.openAppSettings(app.app.id); + }, + icon: const Icon(Icons.settings), + tooltip: tr('settings'), + ), + if (app != null && settingsProvider.showAppWebpage) + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + content: getFullInfoColumn(small: true), + title: Text(app.name), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('continue')), + ), + ], + ); + }, + ); + }, + icon: const Icon(Icons.more_horiz), + tooltip: tr('more'), + ), + if (app?.app.installedVersion != null && + app?.app.installedVersion != app?.app.latestVersion && + !isVersionDetectionStandard && + !trackOnly) + IconButton( + onPressed: app?.downloadProgress != null || updating + ? null + : showMarkUpdatedDialog, + tooltip: tr('markUpdated'), + icon: const Icon(Icons.done), + ), + if ((!isVersionDetectionStandard || trackOnly) && + app?.app.installedVersion != null && + app?.app.installedVersion == app?.app.latestVersion) + IconButton( + onPressed: app?.app == null || updating + ? null + : () { + app!.app.installedVersion = null; + appsProvider.saveApps([app.app]); + }, + icon: const Icon(Icons.restore_rounded), + tooltip: tr('resetInstallStatus'), + ), + const SizedBox(width: 16.0), + Expanded(child: getInstallOrUpdateButton()), + const SizedBox(width: 16.0), + IconButton( + onPressed: app?.downloadProgress != null || updating + ? null + : () { + appsProvider + .removeAppsWithModal( + context, + app != null ? [app.app] : [], + ) + .then((value) { + if (value == true) { + Navigator.of(context).pop(); + } + }); + }, + tooltip: tr('remove'), + icon: const Icon(Icons.delete_outline), + ), + ], + ), + ), + if (app?.downloadProgress != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), + child: LinearProgressIndicator( + value: app!.downloadProgress! >= 0 + ? app.downloadProgress! / 100 + : null, + ), + ), + ], + ), + ); appScreenAppBar() => AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.pop(context); - }, - ), - ); + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ), + ); return Scaffold( - appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(), - backgroundColor: Theme.of(context).colorScheme.surface, - body: RefreshIndicator( - child: settingsProvider.showAppWebpage - ? getAppWebView() - : CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Column(children: [getFullInfoColumn()])), - ], + appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(), + backgroundColor: Theme.of(context).colorScheme.surface, + body: RefreshIndicator( + child: settingsProvider.showAppWebpage + ? getAppWebView() + : CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column(children: [getFullInfoColumn()]), ), - onRefresh: () async { - if (app != null) { - getUpdate(app.app.id); - } - }), - bottomSheet: getBottomSheetMenu()); + ], + ), + onRefresh: () async { + if (app != null) { + getUpdate(app.app.id); + } + }, + ), + bottomSheet: getBottomSheetMenu(), + ); } } diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 4ef8cf9..e029420 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -26,78 +26,91 @@ class AppsPage extends StatefulWidget { State createState() => AppsPageState(); } -showChangeLogDialog(BuildContext context, App app, String? changesUrl, - AppSource appSource, String changeLog) { +showChangeLogDialog( + BuildContext context, + App app, + String? changesUrl, + AppSource appSource, + String changeLog, +) { showDialog( - context: context, - builder: (BuildContext context) { - return GeneratedFormModal( - title: tr('changes'), - items: const [], - message: app.latestVersion, - additionalWidgets: [ - changesUrl != null - ? GestureDetector( - child: Text( - changesUrl, - style: const TextStyle( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic), + context: context, + builder: (BuildContext context) { + return GeneratedFormModal( + title: tr('changes'), + items: const [], + message: app.latestVersion, + additionalWidgets: [ + changesUrl != null + ? GestureDetector( + child: Text( + changesUrl, + style: const TextStyle( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, ), - onTap: () { - launchUrlString(changesUrl, - mode: LaunchMode.externalApplication); - }, - ) - : const SizedBox.shrink(), - changesUrl != null - ? const SizedBox( - height: 16, - ) - : const SizedBox.shrink(), - appSource.changeLogIfAnyIsMarkDown - ? SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height - 350, - child: Markdown( - styleSheet: MarkdownStyleSheet( - blockquoteDecoration: BoxDecoration( - color: Theme.of(context).cardColor)), - data: changeLog, - onTapLink: (text, href, title) { - if (href != null) { - launchUrlString( - href.startsWith('http://') || - href.startsWith('https://') - ? href - : '${Uri.parse(app.url).origin}/$href', - mode: LaunchMode.externalApplication); - } - }, - extensionSet: md.ExtensionSet( - md.ExtensionSet.gitHubFlavored.blockSyntaxes, - [ - md.EmojiSyntax(), - ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes - ], + ), + onTap: () { + launchUrlString( + changesUrl, + mode: LaunchMode.externalApplication, + ); + }, + ) + : const SizedBox.shrink(), + changesUrl != null + ? const SizedBox(height: 16) + : const SizedBox.shrink(), + appSource.changeLogIfAnyIsMarkDown + ? SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 350, + child: Markdown( + styleSheet: MarkdownStyleSheet( + blockquoteDecoration: BoxDecoration( + color: Theme.of(context).cardColor, ), - )) - : Text(changeLog), - ], - singleNullReturnButton: tr('ok'), - ); - }); + ), + data: changeLog, + onTapLink: (text, href, title) { + if (href != null) { + launchUrlString( + href.startsWith('http://') || + href.startsWith('https://') + ? href + : '${Uri.parse(app.url).origin}/$href', + mode: LaunchMode.externalApplication, + ); + } + }, + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [ + md.EmojiSyntax(), + ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes, + ], + ), + ), + ) + : Text(changeLog), + ], + singleNullReturnButton: tr('ok'), + ); + }, + ); } getChangeLogFn(BuildContext context, App app) { - AppSource appSource = - SourceProvider().getSource(app.url, overrideSource: app.overrideSource); + AppSource appSource = SourceProvider().getSource( + app.url, + overrideSource: app.overrideSource, + ); String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url); String? changeLog = app.changeLog; if (changeLog?.split('\n').length == 1) { if (RegExp( - '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?') - .hasMatch(changeLog!)) { + '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?', + ).hasMatch(changeLog!)) { if (changesUrl == null) { changesUrl = changeLog; changeLog = null; @@ -118,8 +131,10 @@ getChangeLogFn(BuildContext context, App app) { class AppsPageState extends State { AppsFilter filter = AppsFilter(); final AppsFilter neutralFilter = AppsFilter(); - var updatesOnlyFilter = - AppsFilter(includeUptodate: false, includeNonInstalled: false); + var updatesOnlyFilter = AppsFilter( + includeUptodate: false, + includeNonInstalled: false, + ); Set selectedAppIds = {}; DateTime? refreshingSince; @@ -161,14 +176,17 @@ class AppsPageState extends State { setState(() { refreshingSince = DateTime.now(); }); - return appsProvider.checkUpdates().catchError((e) { - showError(e is Map ? e['errors'] : e, context); - return []; - }).whenComplete(() { - setState(() { - refreshingSince = null; - }); - }); + return appsProvider + .checkUpdates() + .catchError((e) { + showError(e is Map ? e['errors'] : e, context); + return []; + }) + .whenComplete(() { + setState(() { + refreshingSince = null; + }); + }); } if (!appsProvider.loadingApps && @@ -234,8 +252,10 @@ class AppsPageState extends State { } if (filter.sourceFilter.isNotEmpty && sourceProvider - .getSource(app.app.url, - overrideSource: app.app.overrideSource) + .getSource( + app.app.url, + overrideSource: app.app.overrideSource, + ) .runtimeType .toString() != filter.sourceFilter) { @@ -247,15 +267,19 @@ class AppsPageState extends State { listedApps.sort((a, b) { int result = 0; if (settingsProvider.sortColumn == SortColumnSettings.authorName) { - result = ((a.author + a.name).toLowerCase()) - .compareTo((b.author + b.name).toLowerCase()); + result = ((a.author + a.name).toLowerCase()).compareTo( + (b.author + b.name).toLowerCase(), + ); } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) { - result = ((a.name + a.author).toLowerCase()) - .compareTo((b.name + b.author).toLowerCase()); + result = ((a.name + a.author).toLowerCase()).compareTo( + (b.name + b.author).toLowerCase(), + ); } else if (settingsProvider.sortColumn == SortColumnSettings.releaseDate) { - result = (a.app.releaseDate)?.compareTo( - b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0)) ?? + result = + (a.app.releaseDate)?.compareTo( + b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0), + ) ?? 0; } return result; @@ -268,15 +292,19 @@ class AppsPageState extends State { var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true); var existingUpdateIdsAllOrSelected = existingUpdates - .where((element) => selectedAppIds.isEmpty - ? listedApps.where((a) => a.app.id == element).isNotEmpty - : selectedAppIds.map((e) => e).contains(element)) + .where( + (element) => selectedAppIds.isEmpty + ? listedApps.where((a) => a.app.id == element).isNotEmpty + : selectedAppIds.map((e) => e).contains(element), + ) .toList(); var newInstallIdsAllOrSelected = appsProvider .findExistingUpdates(nonInstalledOnly: true) - .where((element) => selectedAppIds.isEmpty - ? listedApps.where((a) => a.app.id == element).isNotEmpty - : selectedAppIds.map((e) => e).contains(element)) + .where( + (element) => selectedAppIds.isEmpty + ? listedApps.where((a) => a.app.id == element).isNotEmpty + : selectedAppIds.map((e) => e).contains(element), + ) .toList(); List trackOnlyUpdateIdsAllOrSelected = []; @@ -331,11 +359,12 @@ class AppsPageState extends State { listedApps = [...tempPinned, ...tempNotPinned]; List getListedCategories() { - var temp = listedApps - .map((e) => e.app.categories.isNotEmpty ? e.app.categories : [null]); + var temp = listedApps.map( + (e) => e.app.categories.isNotEmpty ? e.app.categories : [null], + ); return temp.isNotEmpty ? { - ...temp.reduce((v, e) => [...v, ...e]) + ...temp.reduce((v, e) => [...v, ...e]), }.toList() : []; } @@ -345,8 +374,8 @@ class AppsPageState extends State { return a != null && b != null ? a.toLowerCase().compareTo(b.toLowerCase()) : a == null - ? 1 - : -1; + ? 1 + : -1; }); Set selectedApps = listedApps @@ -358,92 +387,106 @@ class AppsPageState extends State { return [ if (listedApps.isEmpty) SliverFillRemaining( - child: Center( - child: Text( - appsProvider.apps.isEmpty - ? appsProvider.loadingApps - ? tr('pleaseWait') - : tr('noApps') - : tr('noAppsForFilter'), - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ))), + child: Center( + child: Text( + appsProvider.apps.isEmpty + ? appsProvider.loadingApps + ? tr('pleaseWait') + : tr('noApps') + : tr('noAppsForFilter'), + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + ), + ), if (refreshingSince != null || appsProvider.loadingApps) SliverToBoxAdapter( child: LinearProgressIndicator( value: appsProvider.loadingApps ? null : appsProvider - .getAppValues() - .where((element) => !(element.app.lastUpdateCheck - ?.isBefore(refreshingSince!) ?? - true)) - .length / - (appsProvider.apps.isNotEmpty - ? appsProvider.apps.length - : 1), + .getAppValues() + .where( + (element) => + !(element.app.lastUpdateCheck?.isBefore( + refreshingSince!, + ) ?? + true), + ) + .length / + (appsProvider.apps.isNotEmpty + ? appsProvider.apps.length + : 1), ), - ) + ), ]; } getUpdateButton(int appIndex) { return IconButton( - visualDensity: VisualDensity.compact, - color: Theme.of(context).colorScheme.primary, - tooltip: - listedApps[appIndex].app.additionalSettings['trackOnly'] == true - ? tr('markUpdated') - : tr('update'), - onPressed: appsProvider.areDownloadsRunning() - ? null - : () { - appsProvider.downloadAndInstallLatestApps( - [listedApps[appIndex].app.id], - globalNavigatorKey.currentContext).catchError((e) { - showError(e, context); - return []; - }); - }, - icon: Icon( - listedApps[appIndex].app.additionalSettings['trackOnly'] == true - ? Icons.check_circle_outline - : Icons.install_mobile)); + visualDensity: VisualDensity.compact, + color: Theme.of(context).colorScheme.primary, + tooltip: + listedApps[appIndex].app.additionalSettings['trackOnly'] == true + ? tr('markUpdated') + : tr('update'), + onPressed: appsProvider.areDownloadsRunning() + ? null + : () { + appsProvider + .downloadAndInstallLatestApps([ + listedApps[appIndex].app.id, + ], globalNavigatorKey.currentContext) + .catchError((e) { + showError(e, context); + return []; + }); + }, + icon: Icon( + listedApps[appIndex].app.additionalSettings['trackOnly'] == true + ? Icons.check_circle_outline + : Icons.install_mobile, + ), + ); } getAppIcon(int appIndex) { return FutureBuilder( - future: appsProvider.updateAppIcon(listedApps[appIndex].app.id), - builder: (ctx, val) { - return listedApps[appIndex].icon != null - ? Image.memory( - listedApps[appIndex].icon!, - gaplessPlayback: true, - opacity: AlwaysStoppedAnimation( - listedApps[appIndex].installedInfo == null ? 0.6 : 1), - ) - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationZ(0.31), - child: Padding( - padding: const EdgeInsets.all(15), - child: Image( - image: const AssetImage( - 'assets/graphics/icon_small.png'), - color: Theme.of(context).brightness == - Brightness.dark - ? Colors.white.withOpacity(0.4) - : Colors.white.withOpacity(0.3), - colorBlendMode: BlendMode.modulate, - gaplessPlayback: true, - ), - )), - ]); - }); + future: appsProvider.updateAppIcon(listedApps[appIndex].app.id), + builder: (ctx, val) { + return listedApps[appIndex].icon != null + ? Image.memory( + listedApps[appIndex].icon!, + gaplessPlayback: true, + opacity: AlwaysStoppedAnimation( + listedApps[appIndex].installedInfo == null ? 0.6 : 1, + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform( + alignment: Alignment.center, + transform: Matrix4.rotationZ(0.31), + child: Padding( + padding: const EdgeInsets.all(15), + child: Image( + image: const AssetImage( + 'assets/graphics/icon_small.png', + ), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.4) + : Colors.white.withOpacity(0.3), + colorBlendMode: BlendMode.modulate, + gaplessPlayback: true, + ), + ), + ), + ], + ); + }, + ); } getVersionText(int appIndex) { @@ -453,15 +496,17 @@ class AppsPageState extends State { getChangesButtonString(int appIndex, bool hasChangeLogFn) { return listedApps[appIndex].app.releaseDate == null ? hasChangeLogFn - ? tr('changes') - : '' - : DateFormat('yyyy-MM-dd') - .format(listedApps[appIndex].app.releaseDate!.toLocal()); + ? tr('changes') + : '' + : DateFormat( + 'yyyy-MM-dd', + ).format(listedApps[appIndex].app.releaseDate!.toLocal()); } getSingleAppHorizTile(int index) { var showChangesFn = getChangeLogFn(context, listedApps[index].app); - var hasUpdate = listedApps[index].app.installedVersion != null && + var hasUpdate = + listedApps[index].app.installedVersion != null && listedApps[index].app.installedVersion != listedApps[index].app.latestVersion; Widget trailingRow = Row( @@ -469,168 +514,195 @@ class AppsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ hasUpdate ? getUpdateButton(index) : const SizedBox.shrink(), - hasUpdate - ? const SizedBox( - width: 5, - ) - : const SizedBox.shrink(), + hasUpdate ? const SizedBox(width: 5) : const SizedBox.shrink(), GestureDetector( - onTap: showChangesFn, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: settingsProvider.highlightTouchTargets && - showChangesFn != null - ? (Theme.of(context).brightness == Brightness.light - ? Theme.of(context).primaryColor - : Theme.of(context).primaryColorLight) - .withAlpha(Theme.of(context).brightness == - Brightness.light - ? 20 - : 40) - : null), - padding: settingsProvider.highlightTouchTargets - ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0) - : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row(mainAxisSize: MainAxisSize.min, children: [ - Container( - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width / 4), - child: Text(getVersionText(index), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - style: isVersionPseudo(listedApps[index].app) - ? TextStyle(fontStyle: FontStyle.italic) - : null)), - ]), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - getChangesButtonString( - index, showChangesFn != null), - style: TextStyle( - fontStyle: FontStyle.italic, - decoration: showChangesFn != null - ? TextDecoration.underline - : TextDecoration.none), + onTap: showChangesFn, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: + settingsProvider.highlightTouchTargets && + showChangesFn != null + ? (Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColorLight) + .withAlpha( + Theme.of(context).brightness == Brightness.light + ? 20 + : 40, ) - ], + : null, + ), + padding: settingsProvider.highlightTouchTargets + ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0) + : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / 4, + ), + child: Text( + getVersionText(index), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + style: isVersionPseudo(listedApps[index].app) + ? TextStyle(fontStyle: FontStyle.italic) + : null, + ), ), ], - ))) + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getChangesButtonString(index, showChangesFn != null), + style: TextStyle( + fontStyle: FontStyle.italic, + decoration: showChangesFn != null + ? TextDecoration.underline + : TextDecoration.none, + ), + ), + ], + ), + ], + ), + ), + ), ], ); - var transparent = - Theme.of(context).colorScheme.surface.withAlpha(0).value; + var transparent = Theme.of( + context, + ).colorScheme.surface.withAlpha(0).value; List stops = [ - ...listedApps[index].app.categories.asMap().entries.map((e) => - ((e.key / (listedApps[index].app.categories.length - 1)) - 0.0001)), - 1 + ...listedApps[index].app.categories.asMap().entries.map( + (e) => + ((e.key / (listedApps[index].app.categories.length - 1)) - + 0.0001), + ), + 1, ]; if (stops.length == 2) { stops[0] = 0.9999; } return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - stops: stops, - begin: const Alignment(-1, 0), - end: const Alignment(-0.97, 0), - colors: [ - ...listedApps[index].app.categories.map((e) => - Color(settingsProvider.categories[e] ?? transparent) - .withAlpha(255)), - Color(transparent) - ])), - child: ListTile( - tileColor: listedApps[index].app.pinned - ? Colors.grey.withOpacity(0.1) - : Colors.transparent, - selectedTileColor: Theme.of(context) - .colorScheme - .primary - .withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1), - selected: - selectedAppIds.map((e) => e).contains(listedApps[index].app.id), - onLongPress: () { - toggleAppSelected(listedApps[index].app); - }, - leading: getAppIcon(index), - title: Text( - maxLines: 1, - listedApps[index].name, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontWeight: listedApps[index].app.pinned - ? FontWeight.bold - : FontWeight.normal, + decoration: BoxDecoration( + gradient: LinearGradient( + stops: stops, + begin: const Alignment(-1, 0), + end: const Alignment(-0.97, 0), + colors: [ + ...listedApps[index].app.categories.map( + (e) => Color( + settingsProvider.categories[e] ?? transparent, + ).withAlpha(255), ), + Color(transparent), + ], + ), + ), + child: ListTile( + tileColor: listedApps[index].app.pinned + ? Colors.grey.withOpacity(0.1) + : Colors.transparent, + selectedTileColor: Theme.of(context).colorScheme.primary.withOpacity( + listedApps[index].app.pinned ? 0.2 : 0.1, + ), + selected: selectedAppIds + .map((e) => e) + .contains(listedApps[index].app.id), + onLongPress: () { + toggleAppSelected(listedApps[index].app); + }, + leading: getAppIcon(index), + title: Text( + maxLines: 1, + listedApps[index].name, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal, ), - subtitle: Text(tr('byX', args: [listedApps[index].author]), - maxLines: 1, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontWeight: listedApps[index].app.pinned - ? FontWeight.bold - : FontWeight.normal)), - trailing: listedApps[index].downloadProgress != null - ? SizedBox( - child: Text( + ), + subtitle: Text( + tr('byX', args: [listedApps[index].author]), + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal, + ), + ), + trailing: listedApps[index].downloadProgress != null + ? SizedBox( + child: Text( listedApps[index].downloadProgress! >= 0 - ? tr('percentProgress', args: [ - listedApps[index] - .downloadProgress! - .toInt() - .toString() - ]) + ? tr( + 'percentProgress', + args: [ + listedApps[index].downloadProgress! + .toInt() + .toString(), + ], + ) : tr('installing'), textAlign: (listedApps[index].downloadProgress! >= 0) ? TextAlign.start : TextAlign.end, - )) - : trailingRow, - onTap: () { - if (selectedAppIds.isNotEmpty) { - toggleAppSelected(listedApps[index].app); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppPage(appId: listedApps[index].app.id)), - ); - } - }, - )); + ), + ) + : trailingRow, + onTap: () { + if (selectedAppIds.isNotEmpty) { + toggleAppSelected(listedApps[index].app); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppPage(appId: listedApps[index].app.id), + ), + ); + } + }, + ), + ); } getCategoryCollapsibleTile(int index) { var tiles = listedApps .asMap() .entries - .where((e) => - e.value.app.categories.contains(listedCategories[index]) || - e.value.app.categories.isEmpty && listedCategories[index] == null) + .where( + (e) => + e.value.app.categories.contains(listedCategories[index]) || + e.value.app.categories.isEmpty && + listedCategories[index] == null, + ) .map((e) => getSingleAppHorizTile(e.key)) .toList(); capFirstChar(String str) => str[0].toUpperCase() + str.substring(1); return ExpansionTile( - initiallyExpanded: true, - title: Text( - capFirstChar(listedCategories[index] ?? tr('noCategory')), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - controlAffinity: ListTileControlAffinity.leading, - trailing: Text(tiles.length.toString()), - children: tiles); + initiallyExpanded: true, + title: Text( + capFirstChar(listedCategories[index] ?? tr('noCategory')), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + controlAffinity: ListTileControlAffinity.leading, + trailing: Text(tiles.length.toString()), + children: tiles, + ); } getSelectAllButton() { @@ -644,7 +716,8 @@ class AppsPageState extends State { Icons.select_all_outlined, color: Theme.of(context).colorScheme.primary, ), - label: Text(listedApps.length.toString())) + label: Text(listedApps.length.toString()), + ) : TextButton.icon( style: const ButtonStyle(visualDensity: VisualDensity.compact), onPressed: () { @@ -658,7 +731,8 @@ class AppsPageState extends State { : Icons.deselect_outlined, color: Theme.of(context).colorScheme.primary, ), - label: Text(selectedAppIds.length.toString())); + label: Text(selectedAppIds.length.toString()), + ); } getMassObtainFunction() { @@ -671,42 +745,72 @@ class AppsPageState extends State { HapticFeedback.heavyImpact(); List formItems = []; if (existingUpdateIdsAllOrSelected.isNotEmpty) { - formItems.add(GeneratedFormSwitch('updates', - label: tr('updateX', args: [ - plural('apps', existingUpdateIdsAllOrSelected.length) - .toLowerCase() - ]), - defaultValue: true)); + formItems.add( + GeneratedFormSwitch( + 'updates', + label: tr( + 'updateX', + args: [ + plural( + 'apps', + existingUpdateIdsAllOrSelected.length, + ).toLowerCase(), + ], + ), + defaultValue: true, + ), + ); } if (newInstallIdsAllOrSelected.isNotEmpty) { - formItems.add(GeneratedFormSwitch('installs', - label: tr('installX', args: [ - plural('apps', newInstallIdsAllOrSelected.length) - .toLowerCase() - ]), - defaultValue: existingUpdateIdsAllOrSelected.isEmpty)); + formItems.add( + GeneratedFormSwitch( + 'installs', + label: tr( + 'installX', + args: [ + plural( + 'apps', + newInstallIdsAllOrSelected.length, + ).toLowerCase(), + ], + ), + defaultValue: existingUpdateIdsAllOrSelected.isEmpty, + ), + ); } if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { - formItems.add(GeneratedFormSwitch('trackonlies', - label: tr('markXTrackOnlyAsUpdated', args: [ - plural('apps', trackOnlyUpdateIdsAllOrSelected.length) - ]), - defaultValue: existingUpdateIdsAllOrSelected.isEmpty && - newInstallIdsAllOrSelected.isEmpty)); + formItems.add( + GeneratedFormSwitch( + 'trackonlies', + label: tr( + 'markXTrackOnlyAsUpdated', + args: [ + plural('apps', trackOnlyUpdateIdsAllOrSelected.length), + ], + ), + defaultValue: + existingUpdateIdsAllOrSelected.isEmpty && + newInstallIdsAllOrSelected.isEmpty, + ), + ); } showDialog?>( - context: context, - builder: (BuildContext ctx) { - var totalApps = existingUpdateIdsAllOrSelected.length + - newInstallIdsAllOrSelected.length + - trackOnlyUpdateIdsAllOrSelected.length; - return GeneratedFormModal( - title: tr('changeX', - args: [plural('apps', totalApps).toLowerCase()]), - items: formItems.map((e) => [e]).toList(), - initValid: true, - ); - }).then((values) async { + context: context, + builder: (BuildContext ctx) { + var totalApps = + existingUpdateIdsAllOrSelected.length + + newInstallIdsAllOrSelected.length + + trackOnlyUpdateIdsAllOrSelected.length; + return GeneratedFormModal( + title: tr( + 'changeX', + args: [plural('apps', totalApps).toLowerCase()], + ), + items: formItems.map((e) => [e]).toList(), + initValid: true, + ); + }, + ).then((values) async { if (values != null) { if (values.isEmpty) { values = getDefaultValuesFromFormItems([formItems]); @@ -726,15 +830,18 @@ class AppsPageState extends State { } appsProvider .downloadAndInstallLatestApps( - toInstall, globalNavigatorKey.currentContext) + toInstall, + globalNavigatorKey.currentContext, + ) .catchError((e) { - showError(e, context); - return []; - }).then((value) { - if (value.isNotEmpty && shouldInstallUpdates) { - showMessage(tr('appsUpdated'), context); - } - }); + showError(e, context); + return []; + }) + .then((value) { + if (value.isNotEmpty && shouldInstallUpdates) { + showMessage(tr('appsUpdated'), context); + } + }); } }); }; @@ -758,42 +865,47 @@ class AppsPageState extends State { } var cont = true; if (showPrompt) { - cont = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('categorize'), - items: const [], - initValid: true, - message: tr('selectedCategorizeWarning'), - ); - }) != + cont = + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('categorize'), + items: const [], + initValid: true, + message: tr('selectedCategorizeWarning'), + ); + }, + ) != null; } if (cont) { // ignore: use_build_context_synchronously await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('categorize'), - items: const [], - initValid: true, - singleNullReturnButton: tr('continue'), - additionalWidgets: [ - CategoryEditorSelector( - preselected: !showPrompt ? preselected ?? {} : {}, - showLabelWhenNotEmpty: false, - onSelected: (categories) { - appsProvider.saveApps(selectedApps.map((e) { + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('categorize'), + items: const [], + initValid: true, + singleNullReturnButton: tr('continue'), + additionalWidgets: [ + CategoryEditorSelector( + preselected: !showPrompt ? preselected ?? {} : {}, + showLabelWhenNotEmpty: false, + onSelected: (categories) { + appsProvider.saveApps( + selectedApps.map((e) { e.categories = categories; return e; - }).toList()); - }, - ) - ], - ); - }); + }).toList(), + ); + }, + ), + ], + ); + }, + ); } } catch (err) { showError(err, context); @@ -803,179 +915,199 @@ class AppsPageState extends State { showMassMarkDialog() { return showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - title: Text(tr('markXSelectedAppsAsUpdated', - args: [selectedAppIds.length.toString()])), - content: Text( - tr('onlyWorksWithNonVersionDetectApps'), - style: const TextStyle( - fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text( + tr( + 'markXSelectedAppsAsUpdated', + args: [selectedAppIds.length.toString()], ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('no'))), - TextButton( - onPressed: () { - HapticFeedback.selectionClick(); - appsProvider.saveApps(selectedApps.map((a) { - if (a.installedVersion != null && - !appsProvider.isVersionDetectionPossible( - appsProvider.apps[a.id])) { - a.installedVersion = a.latestVersion; - } - return a; - }).toList()); + ), + content: Text( + tr('onlyWorksWithNonVersionDetectApps'), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('no')), + ), + TextButton( + onPressed: () { + HapticFeedback.selectionClick(); + appsProvider.saveApps( + selectedApps.map((a) { + if (a.installedVersion != null && + !appsProvider.isVersionDetectionPossible( + appsProvider.apps[a.id], + )) { + a.installedVersion = a.latestVersion; + } + return a; + }).toList(), + ); - Navigator.of(context).pop(); - }, - child: Text(tr('yes'))) - ], - ); - }).whenComplete(() { + Navigator.of(context).pop(); + }, + child: Text(tr('yes')), + ), + ], + ); + }, + ).whenComplete(() { Navigator.of(context).pop(); }); } pinSelectedApps() { var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; - appsProvider.saveApps(selectedApps.map((e) { - e.pinned = pinStatus; - return e; - }).toList()); + appsProvider.saveApps( + selectedApps.map((e) { + e.pinned = pinStatus; + return e; + }).toList(), + ); Navigator.of(context).pop(); } showMoreOptionsDialog() { return showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - content: Padding( - padding: const EdgeInsets.only(top: 6), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton( - onPressed: pinSelectedApps, - child: Text(selectedApps - .where((element) => element.pinned) - .isEmpty - ? tr('pinToTop') - : tr('unpinFromTop'))), - const Divider(), - TextButton( - onPressed: () { + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + content: Padding( + padding: const EdgeInsets.only(top: 6), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton( + onPressed: pinSelectedApps, + child: Text( + selectedApps.where((element) => element.pinned).isEmpty + ? tr('pinToTop') + : tr('unpinFromTop'), + ), + ), + const Divider(), + TextButton( + onPressed: () { + String urls = ''; + for (var a in selectedApps) { + urls += '${a.url}\n'; + } + urls = urls.substring(0, urls.length - 1); + Share.share( + urls, + subject: 'Obtainium - ${tr('appsString')}', + ); + Navigator.of(context).pop(); + }, + child: Text(tr('shareSelectedAppURLs')), + ), + const Divider(), + TextButton( + onPressed: selectedAppIds.isEmpty + ? null + : () { String urls = ''; for (var a in selectedApps) { - urls += '${a.url}\n'; + urls += + 'https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/${Uri.encodeComponent(jsonEncode({'id': a.id, 'url': a.url, 'author': a.author, 'name': a.name, 'preferredApkIndex': a.preferredApkIndex, 'additionalSettings': jsonEncode(a.additionalSettings), 'overrideSource': a.overrideSource}))}\n\n'; } - urls = urls.substring(0, urls.length - 1); - Share.share(urls, - subject: 'Obtainium - ${tr('appsString')}'); - Navigator.of(context).pop(); + Share.share( + urls, + subject: 'Obtainium - ${tr('appsString')}', + ); }, - child: Text(tr('shareSelectedAppURLs'))), - const Divider(), - TextButton( - onPressed: selectedAppIds.isEmpty - ? null - : () { - String urls = ''; - for (var a in selectedApps) { - urls += - 'https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/${Uri.encodeComponent(jsonEncode({ - 'id': a.id, - 'url': a.url, - 'author': a.author, - 'name': a.name, - 'preferredApkIndex': - a.preferredApkIndex, - 'additionalSettings': - jsonEncode(a.additionalSettings), - 'overrideSource': a.overrideSource - }))}\n\n'; - } - Share.share(urls, - subject: - 'Obtainium - ${tr('appsString')}'); - }, - child: Text(tr('shareAppConfigLinks'))), - const Divider(), - TextButton( - onPressed: selectedAppIds.isEmpty - ? null - : () { - var encoder = const JsonEncoder.withIndent(" "); - var exportJSON = encoder.convert( - appsProvider.generateExportJSON( - appIds: selectedApps - .map((e) => e.id) - .toList(), - overrideExportSettings: false)); - String fn = - '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}-count-${selectedApps.length}'; - XFile f = XFile.fromData( - Uint8List.fromList( - utf8.encode(exportJSON)), - mimeType: 'application/json', - name: fn); - Share.shareXFiles([f], - fileNameOverrides: ['$fn.json']); - }, - child: Text( - '${tr('share')} - ${tr('obtainiumExport')}')), - const Divider(), - TextButton( - onPressed: () { - appsProvider - .downloadAppAssets( - selectedApps.map((e) => e.id).toList(), - globalNavigatorKey.currentContext ?? - context) - .catchError((e) => showError( - e, - globalNavigatorKey.currentContext ?? - context)); - Navigator.of(context).pop(); + child: Text(tr('shareAppConfigLinks')), + ), + const Divider(), + TextButton( + onPressed: selectedAppIds.isEmpty + ? null + : () { + var encoder = const JsonEncoder.withIndent(" "); + var exportJSON = encoder.convert( + appsProvider.generateExportJSON( + appIds: selectedApps.map((e) => e.id).toList(), + overrideExportSettings: false, + ), + ); + String fn = + '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}-count-${selectedApps.length}'; + XFile f = XFile.fromData( + Uint8List.fromList(utf8.encode(exportJSON)), + mimeType: 'application/json', + name: fn, + ); + Share.shareXFiles( + [f], + fileNameOverrides: ['$fn.json'], + ); }, - child: Text(tr('downloadX', - args: [tr('releaseAsset').toLowerCase()]))), - const Divider(), - TextButton( - onPressed: appsProvider.areDownloadsRunning() - ? null - : showMassMarkDialog, - child: Text(tr('markSelectedAppsUpdated'))), - ]), + child: Text('${tr('share')} - ${tr('obtainiumExport')}'), + ), + const Divider(), + TextButton( + onPressed: () { + appsProvider + .downloadAppAssets( + selectedApps.map((e) => e.id).toList(), + globalNavigatorKey.currentContext ?? context, + ) + .catchError( + (e) => showError( + e, + globalNavigatorKey.currentContext ?? context, + ), + ); + Navigator.of(context).pop(); + }, + child: Text( + tr('downloadX', args: [tr('releaseAsset').toLowerCase()]), + ), + ), + const Divider(), + TextButton( + onPressed: appsProvider.areDownloadsRunning() + ? null + : showMassMarkDialog, + child: Text(tr('markSelectedAppsUpdated')), + ), + ], ), - ); - }); + ), + ); + }, + ); } getMainBottomButtons() { return [ IconButton( - visualDensity: VisualDensity.compact, - onPressed: getMassObtainFunction(), - tooltip: selectedAppIds.isEmpty - ? tr('installUpdateApps') - : tr('installUpdateSelectedApps'), - icon: const Icon( - Icons.file_download_outlined, - )), + visualDensity: VisualDensity.compact, + onPressed: getMassObtainFunction(), + tooltip: selectedAppIds.isEmpty + ? tr('installUpdateApps') + : tr('installUpdateSelectedApps'), + icon: const Icon(Icons.file_download_outlined), + ), IconButton( visualDensity: VisualDensity.compact, onPressed: selectedAppIds.isEmpty ? null : () { appsProvider.removeAppsWithModal( - context, selectedApps.toList()); + context, + selectedApps.toList(), + ); }, tooltip: tr('removeSelectedApps'), icon: const Icon(Icons.delete_outline_outlined), @@ -997,64 +1129,75 @@ class AppsPageState extends State { showFilterDialog() async { var values = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - var vals = filter.toFormValuesMap(); - return GeneratedFormModal( - initValid: true, - title: tr('filterApps'), - items: [ - [ - GeneratedFormTextField('appName', - label: tr('appName'), - required: false, - defaultValue: vals['appName']), - GeneratedFormTextField('author', - label: tr('author'), - required: false, - defaultValue: vals['author']) - ], - [ - GeneratedFormTextField('appId', - label: tr('appId'), - required: false, - defaultValue: vals['appId']) - ], - [ - GeneratedFormSwitch('upToDateApps', - label: tr('upToDateApps'), - defaultValue: vals['upToDateApps']) - ], - [ - GeneratedFormSwitch('nonInstalledApps', - label: tr('nonInstalledApps'), - defaultValue: vals['nonInstalledApps']) - ], - [ - GeneratedFormDropdown( - 'sourceFilter', - label: tr('appSource'), - defaultValue: filter.sourceFilter, - [ - MapEntry('', tr('none')), - ...sourceProvider.sources.map( - (e) => MapEntry(e.runtimeType.toString(), e.name)) - ]) - ] - ], - additionalWidgets: [ - const SizedBox( - height: 16, + context: context, + builder: (BuildContext ctx) { + var vals = filter.toFormValuesMap(); + return GeneratedFormModal( + initValid: true, + title: tr('filterApps'), + items: [ + [ + GeneratedFormTextField( + 'appName', + label: tr('appName'), + required: false, + defaultValue: vals['appName'], + ), + GeneratedFormTextField( + 'author', + label: tr('author'), + required: false, + defaultValue: vals['author'], ), - CategoryEditorSelector( - preselected: filter.categoryFilter, - onSelected: (categories) { - filter.categoryFilter = categories.toSet(); - }, - ) ], - ); - }); + [ + GeneratedFormTextField( + 'appId', + label: tr('appId'), + required: false, + defaultValue: vals['appId'], + ), + ], + [ + GeneratedFormSwitch( + 'upToDateApps', + label: tr('upToDateApps'), + defaultValue: vals['upToDateApps'], + ), + ], + [ + GeneratedFormSwitch( + 'nonInstalledApps', + label: tr('nonInstalledApps'), + defaultValue: vals['nonInstalledApps'], + ), + ], + [ + GeneratedFormDropdown( + 'sourceFilter', + label: tr('appSource'), + defaultValue: filter.sourceFilter, + [ + MapEntry('', tr('none')), + ...sourceProvider.sources.map( + (e) => MapEntry(e.runtimeType.toString(), e.name), + ), + ], + ), + ], + ], + additionalWidgets: [ + const SizedBox(height: 16), + CategoryEditorSelector( + preselected: filter.categoryFilter, + onSelected: (categories) { + filter.categoryFilter = categories.toSet(); + }, + ), + ], + ); + }, + ); if (values != null) { setState(() { filter.setFormValuesFromMap(values); @@ -1068,30 +1211,30 @@ class AppsPageState extends State { children: [ getSelectAllButton(), IconButton( - color: Theme.of(context).colorScheme.primary, - style: const ButtonStyle(visualDensity: VisualDensity.compact), - tooltip: isFilterOff - ? tr('filterApps') - : '${tr('filter')} - ${tr('remove')}', - onPressed: isFilterOff - ? showFilterDialog - : () { - setState(() { - filter = AppsFilter(); - }); - }, - icon: Icon(isFilterOff - ? Icons.search_rounded - : Icons.search_off_rounded)), - const SizedBox( - width: 10, + color: Theme.of(context).colorScheme.primary, + style: const ButtonStyle(visualDensity: VisualDensity.compact), + tooltip: isFilterOff + ? tr('filterApps') + : '${tr('filter')} - ${tr('remove')}', + onPressed: isFilterOff + ? showFilterDialog + : () { + setState(() { + filter = AppsFilter(); + }); + }, + icon: Icon( + isFilterOff ? Icons.search_rounded : Icons.search_off_rounded, + ), ), + const SizedBox(width: 10), const VerticalDivider(), Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: getMainBottomButtons(), - )), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: getMainBottomButtons(), + ), + ), ], ); } @@ -1101,38 +1244,45 @@ class AppsPageState extends State { !(listedCategories.isEmpty || (listedCategories.length == 1 && listedCategories[0] == null)) ? SliverList( - delegate: - SliverChildBuilderDelegate((BuildContext context, int index) { - return getCategoryCollapsibleTile(index); - }, childCount: listedCategories.length)) + delegate: SliverChildBuilderDelegate(( + BuildContext context, + int index, + ) { + return getCategoryCollapsibleTile(index); + }, childCount: listedCategories.length), + ) : SliverList( - delegate: - SliverChildBuilderDelegate((BuildContext context, int index) { - return getSingleAppHorizTile(index); - }, childCount: listedApps.length)); + delegate: SliverChildBuilderDelegate(( + BuildContext context, + int index, + ) { + return getSingleAppHorizTile(index); + }, childCount: listedApps.length), + ); } return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: RefreshIndicator( - key: _refreshIndicatorKey, - onRefresh: refresh, - child: Scrollbar( - interactive: true, - controller: scrollController, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: scrollController, - slivers: [ - CustomAppBar(title: tr('appsString')), - ...getLoadingWidgets(), - getDisplayedList() - ]))), + key: _refreshIndicatorKey, + onRefresh: refresh, + child: Scrollbar( + interactive: true, + controller: scrollController, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: scrollController, + slivers: [ + CustomAppBar(title: tr('appsString')), + ...getLoadingWidgets(), + getDisplayedList(), + ], + ), + ), + ), persistentFooterButtons: appsProvider.apps.isEmpty ? null - : [ - getFilterButtonsRow(), - ], + : [getFilterButtonsRow()], ); } } @@ -1146,14 +1296,15 @@ class AppsFilter { late Set categoryFilter; late String sourceFilter; - AppsFilter( - {this.nameFilter = '', - this.authorFilter = '', - this.idFilter = '', - this.includeUptodate = true, - this.includeNonInstalled = true, - this.categoryFilter = const {}, - this.sourceFilter = ''}); + AppsFilter({ + this.nameFilter = '', + this.authorFilter = '', + this.idFilter = '', + this.includeUptodate = true, + this.includeNonInstalled = true, + this.categoryFilter = const {}, + this.sourceFilter = '', + }); Map toFormValuesMap() { return { @@ -1162,7 +1313,7 @@ class AppsFilter { 'appId': idFilter, 'upToDateApps': includeUptodate, 'nonInstalledApps': includeNonInstalled, - 'sourceFilter': sourceFilter + 'sourceFilter': sourceFilter, }; } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 171f696..ee45ed2 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -43,13 +43,22 @@ class _HomePageState extends State { bool isLinkActivity = false; List pages = [ - NavigationPageItem(tr('appsString'), Icons.apps, - AppsPage(key: GlobalKey())), NavigationPageItem( - tr('addApp'), Icons.add, AddAppPage(key: GlobalKey())), + tr('appsString'), + Icons.apps, + AppsPage(key: GlobalKey()), + ), NavigationPageItem( - tr('importExport'), Icons.import_export, const ImportExportPage()), - NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()) + tr('addApp'), + Icons.add, + AddAppPage(key: GlobalKey()), + ), + NavigationPageItem( + tr('importExport'), + Icons.import_export, + const ImportExportPage(), + ), + NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()), ]; @override @@ -60,63 +69,69 @@ class _HomePageState extends State { var sp = context.read(); if (!sp.welcomeShown) { await showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - title: Text(tr('welcome')), - content: Column( - mainAxisSize: MainAxisSize.min, - spacing: 20, - children: [ - Text(tr('documentationLinksNote')), - GestureDetector( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text(tr('welcome')), + content: Column( + mainAxisSize: MainAxisSize.min, + spacing: 20, + children: [ + Text(tr('documentationLinksNote')), + GestureDetector( + onTap: () { + launchUrlString( + 'https://github.com/ImranR98/Obtainium/blob/main/README.md', + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + 'https://github.com/ImranR98/Obtainium/blob/main/README.md', + style: const TextStyle( + decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(tr('batteryOptimizationNote')), + GestureDetector( onTap: () { - launchUrlString( - 'https://github.com/ImranR98/Obtainium/blob/main/README.md', - mode: LaunchMode.externalApplication); + final intent = AndroidIntent( + action: + 'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS', + package: + obtainiumId, // Replace with your app's package name + ); + + intent.launch(); }, child: Text( - 'https://github.com/ImranR98/Obtainium/blob/main/README.md', + tr('settings'), style: const TextStyle( - decoration: TextDecoration.underline, - fontWeight: FontWeight.bold), - )), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(tr('batteryOptimizationNote')), - GestureDetector( - onTap: () { - final intent = AndroidIntent( - action: - 'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS', - package: - obtainiumId, // Replace with your app's package name - ); - - intent.launch(); - }, - child: Text( - tr('settings'), - style: const TextStyle( - decoration: TextDecoration.underline, - fontWeight: FontWeight.bold), + decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, ), - ) - ], - ) - ], - ), - actions: [ - TextButton( - onPressed: () { - sp.welcomeShown = true; - Navigator.of(context).pop(null); - }, - child: Text(tr('ok'))), + ), + ), + ], + ), ], - ); - }); + ), + actions: [ + TextButton( + onPressed: () { + sp.welcomeShown = true; + Navigator.of(context).pop(null); + }, + child: Text(tr('ok')), + ), + ], + ); + }, + ); } }); } @@ -126,13 +141,12 @@ class _HomePageState extends State { goToAddApp(String data) async { switchToPage(1); - while ( - (pages[1].widget.key as GlobalKey?)?.currentState == - null) { + while ((pages[1].widget.key as GlobalKey?) + ?.currentState == + null) { await Future.delayed(const Duration(microseconds: 1)); } - (pages[1].widget.key as GlobalKey?) - ?.currentState + (pages[1].widget.key as GlobalKey?)?.currentState ?.linkFn(data); } @@ -146,44 +160,49 @@ class _HomePageState extends State { } 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'), - ) - ], - ) - ], - ); - }) != + 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 appsProvider = context.read(); - var result = await appsProvider.import(action == 'app' - ? '{ "apps": [$dataStr] }' - : '{ "apps": $dataStr }'); + var result = await appsProvider.import( + action == 'app' + ? '{ "apps": [$dataStr] }' + : '{ "apps": $dataStr }', + ); // ignore: use_build_context_synchronously showMessage( - tr('importedX', args: [plural('apps', result.key.length)]), - context); + tr('importedX', args: [plural('apps', result.key.length)]), + context, + ); await appsProvider .checkUpdates(specificIds: result.key.map((e) => e.id).toList()) .catchError((e) { - if (e is Map && e['errors'] is MultiAppMultiError) { - showError(e['errors'].toString(), context); - } - return []; - }); + if (e is Map && e['errors'] is MultiAppMultiError) { + showError(e['errors'].toString(), context); + } + return []; + }); } } else { throw ObtainiumError(tr('unknown')); @@ -211,7 +230,8 @@ class _HomePageState extends State { } setIsReversing(int targetIndex) { - bool reversing = selectedIndexHistory.isNotEmpty && + bool reversing = + selectedIndexHistory.isNotEmpty && selectedIndexHistory.last > targetIndex; setState(() { isReversing = reversing; @@ -259,65 +279,71 @@ class _HomePageState extends State { prevIsLoading = appsProvider.loadingApps; return WillPopScope( - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: PageTransitionSwitcher( - duration: Duration( - milliseconds: - settingsProvider.disablePageTransitions ? 0 : 300), - reverse: settingsProvider.reversePageTransitions - ? !isReversing - : isReversing, - transitionBuilder: ( - Widget child, - Animation animation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, - child: pages - .elementAt(selectedIndexHistory.isEmpty - ? 0 - : selectedIndexHistory.last) - .widget, + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: PageTransitionSwitcher( + duration: Duration( + milliseconds: settingsProvider.disablePageTransitions ? 0 : 300, ), - bottomNavigationBar: NavigationBar( - destinations: pages - .map((e) => - NavigationDestination(icon: Icon(e.icon), label: e.title)) - .toList(), - onDestinationSelected: (int index) async { - HapticFeedback.selectionClick(); - switchToPage(index); - }, - selectedIndex: + reverse: settingsProvider.reversePageTransitions + ? !isReversing + : isReversing, + transitionBuilder: + ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: pages + .elementAt( selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last, - ), + ) + .widget, ), - onWillPop: () async { - if (isLinkActivity && - selectedIndexHistory.length == 1 && - selectedIndexHistory.last == 1) { - return true; - } - setIsReversing(selectedIndexHistory.length >= 2 + bottomNavigationBar: NavigationBar( + destinations: pages + .map( + (e) => + NavigationDestination(icon: Icon(e.icon), label: e.title), + ) + .toList(), + onDestinationSelected: (int index) async { + HapticFeedback.selectionClick(); + switchToPage(index); + }, + selectedIndex: selectedIndexHistory.isEmpty + ? 0 + : selectedIndexHistory.last, + ), + ), + onWillPop: () async { + if (isLinkActivity && + selectedIndexHistory.length == 1 && + selectedIndexHistory.last == 1) { + return true; + } + setIsReversing( + selectedIndexHistory.length >= 2 ? selectedIndexHistory.reversed.toList()[1] - : 0); - if (selectedIndexHistory.isNotEmpty) { - setState(() { - selectedIndexHistory.removeLast(); - }); - return false; - } - return !(pages[0].widget.key as GlobalKey) - .currentState - ?.clearSelected(); - }); + : 0, + ); + if (selectedIndexHistory.isNotEmpty) { + setState(() { + selectedIndexHistory.removeLast(); + }); + return false; + } + return !(pages[0].widget.key as GlobalKey).currentState + ?.clearSelected(); + }, + ); } @override diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 2503f6e..cfe4beb 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -45,60 +45,72 @@ class _ImportExportPageState extends State { urlListImport({String? initValue, bool overrideInitValid = false}) { showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - initValid: overrideInitValid, - title: tr('importFromURLList'), - items: [ - [ - GeneratedFormTextField('appURLList', - defaultValue: initValue ?? '', - label: tr('appURLList'), - max: 7, - additionalValidators: [ - (dynamic value) { - if (value != null && value.isNotEmpty) { - var lines = value.trim().split('\n'); - for (int i = 0; i < lines.length; i++) { - try { - sourceProvider.getSource(lines[i]); - } catch (e) { - return '${tr('line')} ${i + 1}: $e'; - } - } + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + initValid: overrideInitValid, + title: tr('importFromURLList'), + items: [ + [ + GeneratedFormTextField( + 'appURLList', + defaultValue: initValue ?? '', + label: tr('appURLList'), + max: 7, + additionalValidators: [ + (dynamic value) { + if (value != null && value.isNotEmpty) { + var lines = value.trim().split('\n'); + for (int i = 0; i < lines.length; i++) { + try { + sourceProvider.getSource(lines[i]); + } catch (e) { + return '${tr('line')} ${i + 1}: $e'; } - return null; } - ]) - ] + } + return null; + }, + ], + ), ], - ); - }).then((values) { + ], + ); + }, + ).then((values) { if (values != null) { var urls = (values['appURLList'] as String).split('\n'); setState(() { importInProgress = true; }); - appsProvider.addAppsByURL(urls).then((errors) { - if (errors.isEmpty) { - showMessage(tr('importedX', args: [plural('apps', urls.length)]), - context); - } else { - showDialog( - context: context, - builder: (BuildContext ctx) { - return ImportErrorDialog( - urlsLength: urls.length, errors: errors); - }); - } - }).catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); + appsProvider + .addAppsByURL(urls) + .then((errors) { + if (errors.isEmpty) { + showMessage( + tr('importedX', args: [plural('apps', urls.length)]), + context, + ); + } else { + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: urls.length, + errors: errors, + ); + }, + ); + } + }) + .catchError((e) { + showError(e, context); + }) + .whenComplete(() { + setState(() { + importInProgress = false; + }); + }); } }); } @@ -107,213 +119,250 @@ class _ImportExportPageState extends State { HapticFeedback.selectionClick(); appsProvider .export( - pickOnly: - pickOnly || (await settingsProvider.getExportDir()) == null, - sp: settingsProvider) + pickOnly: + pickOnly || (await settingsProvider.getExportDir()) == null, + sp: settingsProvider, + ) .then((String? result) { - if (result != null) { - showMessage(tr('exportedTo', args: [result]), context); - } - }).catchError((e) { - showError(e, context); - }); + if (result != null) { + showMessage(tr('exportedTo', args: [result]), context); + } + }) + .catchError((e) { + showError(e, context); + }); } runObtainiumImport() { HapticFeedback.selectionClick(); - FilePicker.platform.pickFiles().then((result) { - setState(() { - importInProgress = true; - }); - if (result != null) { - String data = File(result.files.single.path!).readAsStringSync(); - try { - jsonDecode(data); - } catch (e) { - throw ObtainiumError(tr('invalidInput')); - } - appsProvider.import(data).then((value) { - var cats = settingsProvider.categories; - appsProvider.apps.forEach((key, value) { - for (var c in value.app.categories) { - if (!cats.containsKey(c)) { - cats[c] = generateRandomLightColor().value; - } - } + FilePicker.platform + .pickFiles() + .then((result) { + setState(() { + importInProgress = true; + }); + if (result != null) { + String data = File(result.files.single.path!).readAsStringSync(); + try { + jsonDecode(data); + } catch (e) { + throw ObtainiumError(tr('invalidInput')); + } + appsProvider.import(data).then((value) { + var cats = settingsProvider.categories; + appsProvider.apps.forEach((key, value) { + for (var c in value.app.categories) { + if (!cats.containsKey(c)) { + cats[c] = generateRandomLightColor().value; + } + } + }); + appsProvider.addMissingCategories(settingsProvider); + showMessage( + '${tr('importedX', args: [plural('apps', value.key.length)])}${value.value ? ' + ${tr('settings')}' : ''}', + context, + ); + }); + } else { + // User canceled the picker + } + }) + .catchError((e) { + showError(e, context); + }) + .whenComplete(() { + setState(() { + importInProgress = false; }); - appsProvider.addMissingCategories(settingsProvider); - showMessage( - '${tr('importedX', args: [ - plural('apps', value.key.length) - ])}${value.value ? ' + ${tr('settings')}' : ''}', - context); }); - } else { - // User canceled the picker - } - }).catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); } runUrlImport() { FilePicker.platform.pickFiles().then((result) { if (result != null) { urlListImport( - overrideInitValid: true, - initValue: RegExp('https?://[^"]+') - .allMatches( - File(result.files.single.path!).readAsStringSync()) - .map((e) => e.input.substring(e.start, e.end)) - .toSet() - .toList() - .where((url) { - try { - sourceProvider.getSource(url); - return true; - } catch (e) { - return false; - } - }).join('\n')); + overrideInitValid: true, + initValue: RegExp('https?://[^"]+') + .allMatches(File(result.files.single.path!).readAsStringSync()) + .map((e) => e.input.substring(e.start, e.end)) + .toSet() + .toList() + .where((url) { + try { + sourceProvider.getSource(url); + return true; + } catch (e) { + return false; + } + }) + .join('\n'), + ); } }); } runSourceSearch(AppSource source) { () async { - var values = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('searchX', args: [source.name]), - items: [ - [ - GeneratedFormTextField('searchQuery', + var values = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('searchX', args: [source.name]), + items: [ + [ + GeneratedFormTextField( + 'searchQuery', label: tr('searchQuery'), - required: source.name != FDroidRepo().name) - ], - ...source.searchQuerySettingFormItems.map((e) => [e]), - [ - GeneratedFormTextField('url', + required: source.name != FDroidRepo().name, + ), + ], + ...source.searchQuerySettingFormItems.map((e) => [e]), + [ + GeneratedFormTextField( + 'url', label: source.hosts.isNotEmpty ? tr('overrideSource') : plural('url', 1).substring(2), - defaultValue: - source.hosts.isNotEmpty ? source.hosts[0] : '', - required: true) + defaultValue: source.hosts.isNotEmpty + ? source.hosts[0] + : '', + required: true, + ), + ], ], - ], + ); + }, + ); + if (values != null) { + setState(() { + importInProgress = true; + }); + if (source.hosts.isEmpty || values['url'] != source.hosts[0]) { + source = sourceProvider.getSource( + values['url'], + overrideSource: source.runtimeType.toString(), + ); + } + var urlsWithDescriptions = await source.search( + values['searchQuery'] as String, + querySettings: values, ); - }); - if (values != null) { - setState(() { - importInProgress = true; - }); - if (source.hosts.isEmpty || values['url'] != source.hosts[0]) { - source = sourceProvider.getSource(values['url'], - overrideSource: source.runtimeType.toString()); - } - var urlsWithDescriptions = await source - .search(values['searchQuery'] as String, querySettings: values); - if (urlsWithDescriptions.isNotEmpty) { - var selectedUrls = - // ignore: use_build_context_synchronously - await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return SelectionModal( - entries: urlsWithDescriptions, - selectedByDefault: false, - ); - }); - if (selectedUrls != null && selectedUrls.isNotEmpty) { - var errors = await appsProvider.addAppsByURL(selectedUrls, - sourceOverride: source); - if (errors.isEmpty) { - // ignore: use_build_context_synchronously - showMessage( - tr('importedX', - args: [plural('apps', selectedUrls.length)]), - context); + if (urlsWithDescriptions.isNotEmpty) { + var selectedUrls = + // ignore: use_build_context_synchronously + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return SelectionModal( + entries: urlsWithDescriptions, + selectedByDefault: false, + ); + }, + ); + if (selectedUrls != null && selectedUrls.isNotEmpty) { + var errors = await appsProvider.addAppsByURL( + selectedUrls, + sourceOverride: source, + ); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showMessage( + tr( + 'importedX', + args: [plural('apps', selectedUrls.length)], + ), + context, + ); + } else { + // ignore: use_build_context_synchronously + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: selectedUrls.length, + errors: errors, + ); + }, + ); + } + } } else { - // ignore: use_build_context_synchronously - showDialog( - context: context, - builder: (BuildContext ctx) { - return ImportErrorDialog( - urlsLength: selectedUrls.length, errors: errors); - }); + throw ObtainiumError(tr('noResults')); } } - } else { - throw ObtainiumError(tr('noResults')); - } - } - }() + }() .catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); + showError(e, context); + }) + .whenComplete(() { + setState(() { + importInProgress = false; + }); + }); } runMassSourceImport(MassAppUrlSource source) { () async { - var values = await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('importX', args: [source.name]), - items: source.requiredArgs - .map((e) => [GeneratedFormTextField(e, label: e)]) - .toList(), + var values = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('importX', args: [source.name]), + items: source.requiredArgs + .map((e) => [GeneratedFormTextField(e, label: e)]) + .toList(), + ); + }, + ); + if (values != null) { + setState(() { + importInProgress = true; + }); + var urlsWithDescriptions = await source.getUrlsWithDescriptions( + values.values.map((e) => e.toString()).toList(), ); - }); - if (values != null) { - setState(() { - importInProgress = true; - }); - var urlsWithDescriptions = await source.getUrlsWithDescriptions( - values.values.map((e) => e.toString()).toList()); - var selectedUrls = - // ignore: use_build_context_synchronously - await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return SelectionModal(entries: urlsWithDescriptions); - }); - if (selectedUrls != null) { - var errors = await appsProvider.addAppsByURL(selectedUrls); - if (errors.isEmpty) { - // ignore: use_build_context_synchronously - showMessage( - tr('importedX', args: [plural('apps', selectedUrls.length)]), - context); - } else { - // ignore: use_build_context_synchronously - showDialog( - context: context, - builder: (BuildContext ctx) { - return ImportErrorDialog( - urlsLength: selectedUrls.length, errors: errors); - }); + var selectedUrls = + // ignore: use_build_context_synchronously + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return SelectionModal(entries: urlsWithDescriptions); + }, + ); + if (selectedUrls != null) { + var errors = await appsProvider.addAppsByURL(selectedUrls); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showMessage( + tr( + 'importedX', + args: [plural('apps', selectedUrls.length)], + ), + context, + ); + } else { + // ignore: use_build_context_synchronously + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: selectedUrls.length, + errors: errors, + ); + }, + ); + } + } } - } - } - }() + }() .catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); + showError(e, context); + }) + .whenComplete(() { + setState(() { + importInProgress = false; + }); + }); } var sourceStrings = >{}; @@ -322,228 +371,231 @@ class _ImportExportPageState extends State { }); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: CustomScrollView(slivers: [ + backgroundColor: Theme.of(context).colorScheme.surface, + body: CustomScrollView( + slivers: [ CustomAppBar(title: tr('importExport')), SliverFillRemaining( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FutureBuilder( - future: settingsProvider.getExportDir(), - builder: (context, snapshot) { - return Column( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FutureBuilder( + future: settingsProvider.getExportDir(), + builder: (context, snapshot) { + return Column( + children: [ + Row( children: [ - Row( - children: [ - Expanded( - child: TextButton( - style: outlineButtonStyle, - onPressed: appsProvider.apps.isEmpty || - importInProgress - ? null - : () { - runObtainiumExport(pickOnly: true); - }, - child: Text(tr('pickExportDir'), - textAlign: TextAlign.center), - )), - const SizedBox( - width: 16, + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: + appsProvider.apps.isEmpty || + importInProgress + ? null + : () { + runObtainiumExport(pickOnly: true); + }, + child: Text( + tr('pickExportDir'), + textAlign: TextAlign.center, ), - Expanded( - child: TextButton( - style: outlineButtonStyle, - onPressed: appsProvider.apps.isEmpty || - importInProgress || - snapshot.data == null - ? null - : runObtainiumExport, - child: Text(tr('obtainiumExport'), - textAlign: TextAlign.center), - )), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: outlineButtonStyle, - onPressed: importInProgress - ? null - : runObtainiumImport, - child: Text(tr('obtainiumImport'), - textAlign: TextAlign.center))), - ], - ), - if (snapshot.data != null) - Column( - children: [ - const SizedBox(height: 16), - GeneratedForm( - items: [ - [ - GeneratedFormSwitch( - 'autoExportOnChanges', - label: tr('autoExportOnChanges'), - defaultValue: settingsProvider - .autoExportOnChanges, - ) - ], - [ - GeneratedFormSwitch( - 'exportSettings', - label: tr('includeSettings'), - defaultValue: settingsProvider - .exportSettings, - ) - ] - ], - onValueChanges: - (value, valid, isBuilding) { - if (valid && !isBuilding) { - if (value['autoExportOnChanges'] != - null) { - settingsProvider - .autoExportOnChanges = value[ - 'autoExportOnChanges'] == - true; - } - if (value['exportSettings'] != - null) { - settingsProvider.exportSettings = - value['exportSettings'] == - true; - } - } - }), - ], ), + ), + const SizedBox(width: 16), + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: + appsProvider.apps.isEmpty || + importInProgress || + snapshot.data == null + ? null + : runObtainiumExport, + child: Text( + tr('obtainiumExport'), + textAlign: TextAlign.center, + ), + ), + ), ], - ); - }, - ), - if (importInProgress) - const Column( - children: [ - SizedBox( - height: 14, - ), - LinearProgressIndicator(), - SizedBox( - height: 14, - ), - ], - ) - else - Column( - children: [ - const Divider( - height: 32, - ), - Row( + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: importInProgress + ? null + : runObtainiumImport, + child: Text( + tr('obtainiumImport'), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + if (snapshot.data != null) + Column( children: [ - Expanded( - child: TextButton( - onPressed: importInProgress - ? null - : () async { - var searchSourceName = - await showDialog< - List?>( - context: context, - builder: - (BuildContext - ctx) { - return SelectionModal( - title: tr( - 'selectX', - args: [ - tr('source') - ]), - entries: - sourceStrings, - selectedByDefault: - false, - onlyOneSelectionAllowed: - true, - titlesAreLinks: - false, - ); - }) ?? - []; - var searchSource = - sourceProvider.sources - .where((e) => - searchSourceName - .contains( - e.name)) - .toList(); - if (searchSource.isNotEmpty) { - runSourceSearch( - searchSource[0]); - } - }, - child: Text(tr('searchX', args: [ - tr('source').toLowerCase() - ])))), + const SizedBox(height: 16), + GeneratedForm( + items: [ + [ + GeneratedFormSwitch( + 'autoExportOnChanges', + label: tr('autoExportOnChanges'), + defaultValue: settingsProvider + .autoExportOnChanges, + ), + ], + [ + GeneratedFormSwitch( + 'exportSettings', + label: tr('includeSettings'), + defaultValue: + settingsProvider.exportSettings, + ), + ], + ], + onValueChanges: (value, valid, isBuilding) { + if (valid && !isBuilding) { + if (value['autoExportOnChanges'] != + null) { + settingsProvider.autoExportOnChanges = + value['autoExportOnChanges'] == + true; + } + if (value['exportSettings'] != null) { + settingsProvider.exportSettings = + value['exportSettings'] == true; + } + } + }, + ), ], ), - const SizedBox(height: 8), - TextButton( - onPressed: - importInProgress ? null : urlListImport, + ], + ); + }, + ), + if (importInProgress) + const Column( + children: [ + SizedBox(height: 14), + LinearProgressIndicator(), + SizedBox(height: 14), + ], + ) + else + Column( + children: [ + const Divider(height: 32), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: importInProgress + ? null + : () async { + var searchSourceName = + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return SelectionModal( + title: tr( + 'selectX', + args: [tr('source')], + ), + entries: sourceStrings, + selectedByDefault: false, + onlyOneSelectionAllowed: true, + titlesAreLinks: false, + ); + }, + ) ?? + []; + var searchSource = sourceProvider + .sources + .where( + (e) => searchSourceName.contains( + e.name, + ), + ) + .toList(); + if (searchSource.isNotEmpty) { + runSourceSearch(searchSource[0]); + } + }, child: Text( - tr('importFromURLList'), - )), - const SizedBox(height: 8), - TextButton( - onPressed: - importInProgress ? null : runUrlImport, - child: Text( - tr('importFromURLsInFile'), - )), + tr( + 'searchX', + args: [tr('source').toLowerCase()], + ), + ), + ), + ), ], ), - ...sourceProvider.massUrlSources.map((source) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 8), - TextButton( - onPressed: importInProgress - ? null - : () { - runMassSourceImport(source); - }, - child: Text( - tr('importX', args: [source.name]))) - ])), - const Spacer(), - const Divider( - height: 32, - ), - Text(tr('importedAppsIdDisclaimer'), - textAlign: TextAlign.center, - style: const TextStyle( - fontStyle: FontStyle.italic, fontSize: 12)), - const SizedBox( - height: 8, - ), - ], - ))) - ])); + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress ? null : urlListImport, + child: Text(tr('importFromURLList')), + ), + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress ? null : runUrlImport, + child: Text(tr('importFromURLsInFile')), + ), + ], + ), + ...sourceProvider.massUrlSources.map( + (source) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress + ? null + : () { + runMassSourceImport(source); + }, + child: Text(tr('importX', args: [source.name])), + ), + ], + ), + ), + const Spacer(), + const Divider(height: 32), + Text( + tr('importedAppsIdDisclaimer'), + textAlign: TextAlign.center, + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 12, + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ], + ), + ); } } class ImportErrorDialog extends StatefulWidget { - const ImportErrorDialog( - {super.key, required this.urlsLength, required this.errors}); + const ImportErrorDialog({ + super.key, + required this.urlsLength, + required this.errors, + }); final int urlsLength; final List> errors; @@ -558,41 +610,43 @@ class _ImportErrorDialogState extends State { return AlertDialog( scrollable: true, title: Text(tr('importErrors')), - content: - Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - tr('importedXOfYApps', args: [ - (widget.urlsLength - widget.errors.length).toString(), - widget.urlsLength.toString() - ]), - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Text( - tr('followingURLsHadErrors'), - style: Theme.of(context).textTheme.bodyLarge, - ), - ...widget.errors.map((e) { - return Column( + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + tr( + 'importedXOfYApps', + args: [ + (widget.urlsLength - widget.errors.length).toString(), + widget.urlsLength.toString(), + ], + ), + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 16), + Text( + tr('followingURLsHadErrors'), + style: Theme.of(context).textTheme.bodyLarge, + ), + ...widget.errors.map((e) { + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text(e[0]), - Text( - e[1], - style: const TextStyle(fontStyle: FontStyle.italic), - ) - ]); - }) - ]), + Text(e[1], style: const TextStyle(fontStyle: FontStyle.italic)), + ], + ); + }), + ], + ), actions: [ TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(tr('ok'))) + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(tr('ok')), + ), ], ); } @@ -600,14 +654,15 @@ class _ImportErrorDialogState extends State { // ignore: must_be_immutable class SelectionModal extends StatefulWidget { - SelectionModal( - {super.key, - required this.entries, - this.selectedByDefault = true, - this.onlyOneSelectionAllowed = false, - this.titlesAreLinks = true, - this.title, - this.deselectThese = const []}); + SelectionModal({ + super.key, + required this.entries, + this.selectedByDefault = true, + this.onlyOneSelectionAllowed = false, + this.titlesAreLinks = true, + this.title, + this.deselectThese = const [], + }); String? title; Map> entries; @@ -628,11 +683,12 @@ class _SelectionModalState extends State { super.initState(); for (var entry in widget.entries.entries) { entrySelections.putIfAbsent( - entry, - () => - widget.selectedByDefault && - !widget.onlyOneSelectionAllowed && - !widget.deselectThese.contains(entry.key)); + entry, + () => + widget.selectedByDefault && + !widget.onlyOneSelectionAllowed && + !widget.deselectThese.contains(entry.key), + ); } if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) { selectOnlyOne(widget.entries.entries.first.key); @@ -658,8 +714,10 @@ class _SelectionModalState extends State { entrySelections.forEach((key, value) { var searchableText = key.value.isEmpty ? key.key : key.value[0]; if (filterRegex.isEmpty || - RegExp(filterRegex, caseSensitive: false) - .hasMatch(searchableText)) { + RegExp( + filterRegex, + caseSensitive: false, + ).hasMatch(searchableText)) { filteredEntrySelections.putIfAbsent(key, () => value); } }); @@ -667,19 +725,22 @@ class _SelectionModalState extends State { return AlertDialog( scrollable: true, title: Text(widget.title ?? tr('pick')), - content: Column(children: [ - GeneratedForm( + content: Column( + children: [ + GeneratedForm( items: [ [ - GeneratedFormTextField('filter', - label: tr('filter'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) - ] + GeneratedFormTextField( + 'filter', + label: tr('filter'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), + ], ], onValueChanges: (value, valid, isBuilding) { if (valid && !isBuilding) { @@ -689,25 +750,28 @@ class _SelectionModalState extends State { }); } } - }), - ...filteredEntrySelections.keys.map((entry) { - selectThis(bool? value) { - setState(() { - value ??= false; - if (value! && widget.onlyOneSelectionAllowed) { - selectOnlyOne(entry.key); - } else { - entrySelections[entry] = value!; - } - }); - } + }, + ), + ...filteredEntrySelections.keys.map((entry) { + selectThis(bool? value) { + setState(() { + value ??= false; + if (value! && widget.onlyOneSelectionAllowed) { + selectOnlyOne(entry.key); + } else { + entrySelections[entry] = value!; + } + }); + } - var urlLink = GestureDetector( + var urlLink = GestureDetector( onTap: !widget.titlesAreLinks ? null : () { - launchUrlString(entry.key, - mode: LaunchMode.externalApplication); + launchUrlString( + entry.key, + mode: LaunchMode.externalApplication, + ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -715,131 +779,146 @@ class _SelectionModalState extends State { Text( entry.value.isEmpty ? entry.key : entry.value[0], style: TextStyle( - decoration: widget.titlesAreLinks - ? TextDecoration.underline - : null, - fontWeight: FontWeight.bold), + decoration: widget.titlesAreLinks + ? TextDecoration.underline + : null, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.start, ), if (widget.titlesAreLinks) Text( Uri.parse(entry.key).host, style: const TextStyle( - decoration: TextDecoration.underline, fontSize: 12), - ) - ], - )); - - var descriptionText = entry.value.length <= 1 - ? const SizedBox.shrink() - : Text( - entry.value[1].length > 128 - ? '${entry.value[1].substring(0, 128)}...' - : entry.value[1], - style: const TextStyle( - fontStyle: FontStyle.italic, fontSize: 12), - ); - - var selectedEntries = - entrySelections.entries.where((e) => e.value).toList(); - - var singleSelectTile = ListTile( - title: GestureDetector( - onTap: widget.titlesAreLinks - ? null - : () { - selectThis(!(entrySelections[entry] ?? false)); - }, - child: urlLink, - ), - subtitle: entry.value.length <= 1 - ? null - : GestureDetector( - onTap: () { - setState(() { - selectOnlyOne(entry.key); - }); - }, - child: descriptionText, - ), - leading: Radio( - value: entry.key, - groupValue: selectedEntries.isEmpty - ? null - : selectedEntries.first.key.key, - onChanged: (value) { - setState(() { - selectOnlyOne(entry.key); - }); - }, - ), - ); - - var multiSelectTile = Row(children: [ - Checkbox( - value: entrySelections[entry], - onChanged: (value) { - selectThis(value); - }), - const SizedBox( - width: 8, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 8, - ), - GestureDetector( - onTap: widget.titlesAreLinks - ? null - : () { - selectThis(!(entrySelections[entry] ?? false)); - }, - child: urlLink, - ), - entry.value.length <= 1 - ? const SizedBox.shrink() - : GestureDetector( - onTap: () { - selectThis(!(entrySelections[entry] ?? false)); - }, - child: descriptionText, + decoration: TextDecoration.underline, + fontSize: 12, ), - const SizedBox( - height: 8, - ) - ], - )) - ]); + ), + ], + ), + ); - return widget.onlyOneSelectionAllowed - ? singleSelectTile - : multiSelectTile; - }) - ]), + var descriptionText = entry.value.length <= 1 + ? const SizedBox.shrink() + : Text( + entry.value[1].length > 128 + ? '${entry.value[1].substring(0, 128)}...' + : entry.value[1], + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 12, + ), + ); + + var selectedEntries = entrySelections.entries + .where((e) => e.value) + .toList(); + + var singleSelectTile = ListTile( + title: GestureDetector( + onTap: widget.titlesAreLinks + ? null + : () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: urlLink, + ), + subtitle: entry.value.length <= 1 + ? null + : GestureDetector( + onTap: () { + setState(() { + selectOnlyOne(entry.key); + }); + }, + child: descriptionText, + ), + leading: Radio( + value: entry.key, + groupValue: selectedEntries.isEmpty + ? null + : selectedEntries.first.key.key, + onChanged: (value) { + setState(() { + selectOnlyOne(entry.key); + }); + }, + ), + ); + + var multiSelectTile = Row( + children: [ + Checkbox( + value: entrySelections[entry], + onChanged: (value) { + selectThis(value); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 8), + GestureDetector( + onTap: widget.titlesAreLinks + ? null + : () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: urlLink, + ), + entry.value.length <= 1 + ? const SizedBox.shrink() + : GestureDetector( + onTap: () { + selectThis(!(entrySelections[entry] ?? false)); + }, + child: descriptionText, + ), + const SizedBox(height: 8), + ], + ), + ), + ], + ); + + return widget.onlyOneSelectionAllowed + ? singleSelectTile + : multiSelectTile; + }), + ], + ), actions: [ TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('cancel'))), + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('cancel')), + ), TextButton( - onPressed: entrySelections.values.where((b) => b).isEmpty - ? null - : () { - Navigator.of(context).pop(entrySelections.entries + onPressed: entrySelections.values.where((b) => b).isEmpty + ? null + : () { + Navigator.of(context).pop( + entrySelections.entries .where((entry) => entry.value) .map((e) => e.key.key) - .toList()); - }, - child: Text(widget.onlyOneSelectionAllowed + .toList(), + ); + }, + child: Text( + widget.onlyOneSelectionAllowed ? tr('pick') - : tr('selectX', args: [ - entrySelections.values.where((b) => b).length.toString() - ]))) + : tr( + 'selectX', + args: [ + entrySelections.values.where((b) => b).length.toString(), + ], + ), + ), + ), ], ); } diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 65495e9..b102f61 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -38,7 +38,7 @@ class _SettingsPageState extends State { 4320, 10080, 20160, - 43200 + 43200, ]; int updateInterval = 0; late SplineInterpolation updateIntervalInterpolator; // 🤓 @@ -46,14 +46,15 @@ class _SettingsPageState extends State { bool showIntervalLabel = true; final Map, String> colorsNameMap = , String>{ - ColorTools.createPrimarySwatch(obtainiumThemeColor): 'Obtainium' - }; + ColorTools.createPrimarySwatch(obtainiumThemeColor): 'Obtainium', + }; void initUpdateIntervalInterpolator() { List nodes = []; for (final (index, element) in updateIntervalNodes.indexed) { nodes.add( - InterpolationNode(x: index.toDouble() + 1, y: element.toDouble())); + InterpolationNode(x: index.toDouble() + 1, y: element.toDouble()), + ); } updateIntervalInterpolator = SplineInterpolation(nodes: nodes); } @@ -103,13 +104,16 @@ class _SettingsPageState extends State { processIntervalSliderValue(settingsProvider.updateIntervalSliderVal); var followSystemThemeExplanation = FutureBuilder( - builder: (ctx, val) { - return ((val.data?.version.sdkInt ?? 30) < 29) - ? Text(tr('followSystemThemeExplanation'), - style: Theme.of(context).textTheme.labelSmall) - : const SizedBox.shrink(); - }, - future: DeviceInfoPlugin().androidInfo); + builder: (ctx, val) { + return ((val.data?.version.sdkInt ?? 30) < 29) + ? Text( + tr('followSystemThemeExplanation'), + style: Theme.of(context).textTheme.labelSmall, + ) + : const SizedBox.shrink(); + }, + future: DeviceInfoPlugin().androidInfo, + ); Future colorPickerDialog() async { return ColorPicker( @@ -131,10 +135,12 @@ class _SettingsPageState extends State { }, pickerTypeLabels: { ColorPickerType.custom: tr('standard'), - ColorPickerType.wheel: tr('custom') + ColorPickerType.wheel: tr('custom'), }, - title: Text(tr('selectX', args: [tr('colour')]), - style: Theme.of(context).textTheme.titleLarge), + title: Text( + tr('selectX', args: [tr('colour')]), + style: Theme.of(context).textTheme.titleLarge, + ), wheelDiameter: 192, wheelSquareBorderRadius: 32, width: 48, @@ -148,132 +154,142 @@ class _SettingsPageState extends State { showColorName: true, materialNameTextStyle: Theme.of(context).textTheme.bodySmall, colorNameTextStyle: Theme.of(context).textTheme.bodySmall, - copyPasteBehavior: - const ColorPickerCopyPasteBehavior(longPressMenu: true), + copyPasteBehavior: const ColorPickerCopyPasteBehavior( + longPressMenu: true, + ), ).showPickerDialog( context, - transitionBuilder: (BuildContext context, Animation a1, - Animation a2, Widget widget) { - final double curvedValue = Curves.easeInCubic.transform(a1.value); - return Transform( - alignment: Alignment.center, - transform: Matrix4.diagonal3Values(curvedValue, curvedValue, 1), - child: Opacity(opacity: curvedValue, child: widget), - ); - }, + transitionBuilder: + ( + BuildContext context, + Animation a1, + Animation a2, + Widget widget, + ) { + final double curvedValue = Curves.easeInCubic.transform(a1.value); + return Transform( + alignment: Alignment.center, + transform: Matrix4.diagonal3Values(curvedValue, curvedValue, 1), + child: Opacity(opacity: curvedValue, child: widget), + ); + }, transitionDuration: const Duration(milliseconds: 250), ); } var colorPicker = ListTile( - dense: true, - contentPadding: EdgeInsets.zero, - title: Text(tr('selectX', args: [tr('colour')])), - subtitle: Text( - "${ColorTools.nameThatColor(settingsProvider.themeColor)} " - "(${ColorTools.materialNameAndCode(settingsProvider.themeColor, colorSwatchNameMap: colorsNameMap)})"), - trailing: ColorIndicator( - width: 40, - height: 40, - borderRadius: 20, - color: settingsProvider.themeColor, - onSelectFocus: false, - onSelect: () async { - final Color colorBeforeDialog = settingsProvider.themeColor; - if (!(await colorPickerDialog())) { - setState(() { - settingsProvider.themeColor = colorBeforeDialog; - }); - } - })); + dense: true, + contentPadding: EdgeInsets.zero, + title: Text(tr('selectX', args: [tr('colour')])), + subtitle: Text( + "${ColorTools.nameThatColor(settingsProvider.themeColor)} " + "(${ColorTools.materialNameAndCode(settingsProvider.themeColor, colorSwatchNameMap: colorsNameMap)})", + ), + trailing: ColorIndicator( + width: 40, + height: 40, + borderRadius: 20, + color: settingsProvider.themeColor, + onSelectFocus: false, + onSelect: () async { + final Color colorBeforeDialog = settingsProvider.themeColor; + if (!(await colorPickerDialog())) { + setState(() { + settingsProvider.themeColor = colorBeforeDialog; + }); + } + }, + ), + ); var useMaterialThemeSwitch = FutureBuilder( - builder: (ctx, val) { - return ((val.data?.version.sdkInt ?? 0) >= 31) - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('useMaterialYou'))), - Switch( - value: settingsProvider.useMaterialYou, - onChanged: (value) { - settingsProvider.useMaterialYou = value; - }) - ], - ) - : const SizedBox.shrink(); - }, - future: DeviceInfoPlugin().androidInfo); + builder: (ctx, val) { + return ((val.data?.version.sdkInt ?? 0) >= 31) + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('useMaterialYou'))), + Switch( + value: settingsProvider.useMaterialYou, + onChanged: (value) { + settingsProvider.useMaterialYou = value; + }, + ), + ], + ) + : const SizedBox.shrink(); + }, + future: DeviceInfoPlugin().androidInfo, + ); var sortDropdown = DropdownButtonFormField( - isExpanded: true, - decoration: InputDecoration(labelText: tr('appSortBy')), - value: settingsProvider.sortColumn, - items: [ - DropdownMenuItem( - value: SortColumnSettings.authorName, - child: Text(tr('authorName')), - ), - DropdownMenuItem( - value: SortColumnSettings.nameAuthor, - child: Text(tr('nameAuthor')), - ), - DropdownMenuItem( - value: SortColumnSettings.added, - child: Text(tr('asAdded')), - ), - DropdownMenuItem( - value: SortColumnSettings.releaseDate, - child: Text(tr('releaseDate')), - ) - ], - onChanged: (value) { - if (value != null) { - settingsProvider.sortColumn = value; - } - }); + isExpanded: true, + decoration: InputDecoration(labelText: tr('appSortBy')), + value: settingsProvider.sortColumn, + items: [ + DropdownMenuItem( + value: SortColumnSettings.authorName, + child: Text(tr('authorName')), + ), + DropdownMenuItem( + value: SortColumnSettings.nameAuthor, + child: Text(tr('nameAuthor')), + ), + DropdownMenuItem( + value: SortColumnSettings.added, + child: Text(tr('asAdded')), + ), + DropdownMenuItem( + value: SortColumnSettings.releaseDate, + child: Text(tr('releaseDate')), + ), + ], + onChanged: (value) { + if (value != null) { + settingsProvider.sortColumn = value; + } + }, + ); var orderDropdown = DropdownButtonFormField( - isExpanded: true, - decoration: InputDecoration(labelText: tr('appSortOrder')), - value: settingsProvider.sortOrder, - items: [ - DropdownMenuItem( - value: SortOrderSettings.ascending, - child: Text(tr('ascending')), - ), - DropdownMenuItem( - value: SortOrderSettings.descending, - child: Text(tr('descending')), - ), - ], - onChanged: (value) { - if (value != null) { - settingsProvider.sortOrder = value; - } - }); + isExpanded: true, + decoration: InputDecoration(labelText: tr('appSortOrder')), + value: settingsProvider.sortOrder, + items: [ + DropdownMenuItem( + value: SortOrderSettings.ascending, + child: Text(tr('ascending')), + ), + DropdownMenuItem( + value: SortOrderSettings.descending, + child: Text(tr('descending')), + ), + ], + onChanged: (value) { + if (value != null) { + settingsProvider.sortOrder = value; + } + }, + ); var localeDropdown = DropdownButtonFormField( - decoration: InputDecoration(labelText: tr('language')), - value: settingsProvider.forcedLocale, - items: [ - DropdownMenuItem( - value: null, - child: Text(tr('followSystem')), - ), - ...supportedLocales.map((e) => DropdownMenuItem( - value: e.key, - child: Text(e.value), - )) - ], - onChanged: (value) { - settingsProvider.forcedLocale = value; - if (value != null) { - context.setLocale(value); - } else { - settingsProvider.resetLocaleSafe(context); - } - }); + decoration: InputDecoration(labelText: tr('language')), + value: settingsProvider.forcedLocale, + items: [ + DropdownMenuItem(value: null, child: Text(tr('followSystem'))), + ...supportedLocales.map( + (e) => DropdownMenuItem(value: e.key, child: Text(e.value)), + ), + ], + onChanged: (value) { + settingsProvider.forcedLocale = value; + if (value != null) { + context.setLocale(value); + } else { + settingsProvider.resetLocaleSafe(context); + } + }, + ); var intervalSlider = Slider( value: settingsProvider.updateIntervalSliderVal, @@ -302,630 +318,662 @@ class _SettingsPageState extends State { var sourceSpecificFields = sourceProvider.sources.map((e) { if (e.sourceConfigSettingFormItems.isNotEmpty) { return GeneratedForm( - items: e.sourceConfigSettingFormItems.map((e) { - e.defaultValue = settingsProvider.getSettingString(e.key); - return [e]; - }).toList(), - onValueChanges: (values, valid, isBuilding) { - if (valid && !isBuilding) { - values.forEach((key, value) { - settingsProvider.setSettingString(key, value); - }); - } - }); + items: e.sourceConfigSettingFormItems.map((e) { + e.defaultValue = settingsProvider.getSettingString(e.key); + return [e]; + }).toList(), + onValueChanges: (values, valid, isBuilding) { + if (valid && !isBuilding) { + values.forEach((key, value) { + settingsProvider.setSettingString(key, value); + }); + } + }, + ); } else { return Container(); } }); - const height8 = SizedBox( - height: 8, - ); + const height8 = SizedBox(height: 8); - const height16 = SizedBox( - height: 16, - ); + const height16 = SizedBox(height: 16); - const height32 = SizedBox( - height: 32, - ); + const height32 = SizedBox(height: 32); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: CustomScrollView(slivers: [ + backgroundColor: Theme.of(context).colorScheme.surface, + body: CustomScrollView( + slivers: [ CustomAppBar(title: tr('settings')), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(16), - child: settingsProvider.prefs == null - ? const SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('updates'), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary), + child: Padding( + padding: const EdgeInsets.all(16), + child: settingsProvider.prefs == null + ? const SizedBox() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('updates'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + //intervalDropdown, + height16, + if (showIntervalLabel) + SizedBox( + child: Text( + "${tr('bgUpdateCheckInterval')}: $updateIntervalLabel", ), - //intervalDropdown, - height16, - if (showIntervalLabel) - SizedBox( - child: Text( - "${tr('bgUpdateCheckInterval')}: $updateIntervalLabel")) - else - const SizedBox(height: 16), - intervalSlider, - FutureBuilder( - builder: (ctx, val) { - return (settingsProvider.updateInterval > - 0) && - (((val.data?.version.sdkInt ?? 0) >= - 30) || - settingsProvider.useShizuku) - ? Column( - crossAxisAlignment: - CrossAxisAlignment.start, + ) + else + const SizedBox(height: 16), + intervalSlider, + FutureBuilder( + builder: (ctx, val) { + return (settingsProvider.updateInterval > 0) && + (((val.data?.version.sdkInt ?? 0) >= 30) || + settingsProvider.useShizuku) + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + tr('enableBackgroundUpdates'), + ), + ), + Switch( + value: settingsProvider + .enableBackgroundUpdates, + onChanged: (value) { + settingsProvider + .enableBackgroundUpdates = + value; + }, + ), + ], + ), + height8, + Text( + tr('backgroundUpdateReqsExplanation'), + style: Theme.of( + context, + ).textTheme.labelSmall, + ), + Text( + tr('backgroundUpdateLimitsExplanation'), + style: Theme.of( + context, + ).textTheme.labelSmall, + ), + height8, + if (settingsProvider + .enableBackgroundUpdates) + Column( children: [ + height16, Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Flexible( - child: Text(tr( - 'enableBackgroundUpdates'))), + child: Text( + tr('bgUpdatesOnWiFiOnly'), + ), + ), Switch( - value: settingsProvider - .enableBackgroundUpdates, - onChanged: (value) { - settingsProvider - .enableBackgroundUpdates = - value; - }) + value: settingsProvider + .bgUpdatesOnWiFiOnly, + onChanged: (value) { + settingsProvider + .bgUpdatesOnWiFiOnly = + value; + }, + ), ], ), - height8, - Text(tr('backgroundUpdateReqsExplanation'), - style: Theme.of(context) - .textTheme - .labelSmall), - Text(tr('backgroundUpdateLimitsExplanation'), - style: Theme.of(context) - .textTheme - .labelSmall), - height8, - if (settingsProvider - .enableBackgroundUpdates) - Column( - children: [ - height16, - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Flexible( - child: Text(tr( - 'bgUpdatesOnWiFiOnly'))), - Switch( - value: settingsProvider - .bgUpdatesOnWiFiOnly, - onChanged: (value) { - settingsProvider - .bgUpdatesOnWiFiOnly = - value; - }) - ], + height16, + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Flexible( + child: Text( + tr( + 'bgUpdatesWhileChargingOnly', + ), ), - height16, - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Flexible( - child: Text(tr( - 'bgUpdatesWhileChargingOnly'))), - Switch( - value: settingsProvider - .bgUpdatesWhileChargingOnly, - onChanged: (value) { - settingsProvider - .bgUpdatesWhileChargingOnly = - value; - }) - ], - ), - ], - ), + ), + Switch( + value: settingsProvider + .bgUpdatesWhileChargingOnly, + onChanged: (value) { + settingsProvider + .bgUpdatesWhileChargingOnly = + value; + }, + ), + ], + ), ], - ) - : const SizedBox.shrink(); - }, - future: DeviceInfoPlugin().androidInfo), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('checkOnStart'))), - Switch( - value: settingsProvider.checkOnStart, - onChanged: (value) { - settingsProvider.checkOnStart = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text(tr('checkUpdateOnDetailPage'))), - Switch( - value: settingsProvider - .checkUpdateOnDetailPage, - onChanged: (value) { - settingsProvider.checkUpdateOnDetailPage = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text(tr( - 'onlyCheckInstalledOrTrackOnlyApps'))), - Switch( - value: settingsProvider - .onlyCheckInstalledOrTrackOnlyApps, - onChanged: (value) { - settingsProvider - .onlyCheckInstalledOrTrackOnlyApps = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: - Text(tr('removeOnExternalUninstall'))), - Switch( - value: settingsProvider - .removeOnExternalUninstall, - onChanged: (value) { - settingsProvider - .removeOnExternalUninstall = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('parallelDownloads'))), - Switch( - value: settingsProvider.parallelDownloads, - onChanged: (value) { - settingsProvider.parallelDownloads = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(tr( - 'beforeNewInstallsShareToAppVerifier')), - GestureDetector( - onTap: () { - launchUrlString( - 'https://github.com/soupslurpr/AppVerifier', - mode: LaunchMode - .externalApplication); - }, - child: Text( - tr('about'), - style: const TextStyle( - decoration: - TextDecoration.underline, - fontSize: 12), - )), - ], - )), - Switch( - value: settingsProvider - .beforeNewInstallsShareToAppVerifier, - onChanged: (value) { - settingsProvider - .beforeNewInstallsShareToAppVerifier = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('useShizuku'))), - Switch( - value: settingsProvider.useShizuku, - onChanged: (useShizuku) { - if (useShizuku) { - ShizukuApkInstaller.checkPermission() - .then((resCode) { - settingsProvider.useShizuku = - resCode!.startsWith('granted'); - switch (resCode) { - case 'binder_not_found': - showError( - ObtainiumError(tr( - 'shizukuBinderNotFound')), - context); - case 'old_shizuku': - showError( - ObtainiumError( - tr('shizukuOld')), - context); - case 'old_android_with_adb': - showError( - ObtainiumError(tr( - 'shizukuOldAndroidWithADB')), - context); - case 'denied': - showError( - ObtainiumError( - tr('cancelled')), - context); - } - }); - } else { - settingsProvider.useShizuku = false; - } - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - tr('shizukuPretendToBeGooglePlay'))), - Switch( - value: settingsProvider - .shizukuPretendToBeGooglePlay, - onChanged: (value) { - settingsProvider - .shizukuPretendToBeGooglePlay = value; - }) - ], - ), - height32, - Text( - tr('sourceSpecific'), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary), - ), - ...sourceSpecificFields, - height32, - Text( - tr('appearance'), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary), - ), - DropdownButtonFormField( - decoration: - InputDecoration(labelText: tr('theme')), - value: settingsProvider.theme, - items: [ - DropdownMenuItem( - value: ThemeSettings.system, - child: Text(tr('followSystem')), - ), - DropdownMenuItem( - value: ThemeSettings.light, - child: Text(tr('light')), - ), - DropdownMenuItem( - value: ThemeSettings.dark, - child: Text(tr('dark')), + ), + ], ) - ], - onChanged: (value) { - if (value != null) { - settingsProvider.theme = value; - } - }), - height8, - if (settingsProvider.theme == ThemeSettings.system) - followSystemThemeExplanation, - height16, - if (settingsProvider.theme != ThemeSettings.light) - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('useBlackTheme'))), - Switch( - value: settingsProvider.useBlackTheme, - onChanged: (value) { - settingsProvider.useBlackTheme = - value; - }) - ]), - height8, - useMaterialThemeSwitch, - if (!settingsProvider.useMaterialYou) colorPicker, - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: sortDropdown), - const SizedBox( - width: 16, - ), - Expanded(child: orderDropdown), - ], + : const SizedBox.shrink(); + }, + future: DeviceInfoPlugin().androidInfo, + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('checkOnStart'))), + Switch( + value: settingsProvider.checkOnStart, + onChanged: (value) { + settingsProvider.checkOnStart = value; + }, ), - height16, - localeDropdown, - FutureBuilder( - builder: (ctx, val) { - return (val.data?.version.sdkInt ?? 0) >= 34 - ? Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - height16, - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Flexible( - child: Text(tr( - 'useSystemFont'))), - Switch( - value: settingsProvider - .useSystemFont, - onChanged: - (useSystemFont) { - if (useSystemFont) { - NativeFeatures - .loadSystemFont() - .then((val) { - settingsProvider - .useSystemFont = - true; - }); - } else { - settingsProvider - .useSystemFont = - false; - } - }) - ]) - ]) - : const SizedBox.shrink(); - }, - future: DeviceInfoPlugin().androidInfo), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('showWebInAppView'))), - Switch( - value: settingsProvider.showAppWebpage, - onChanged: (value) { - settingsProvider.showAppWebpage = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('pinUpdates'))), - Switch( - value: settingsProvider.pinUpdates, - onChanged: (value) { - settingsProvider.pinUpdates = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - tr('moveNonInstalledAppsToBottom'))), - Switch( - value: settingsProvider.buryNonInstalled, - onChanged: (value) { - settingsProvider.buryNonInstalled = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: Text(tr('groupByCategory'))), - Switch( - value: settingsProvider.groupByCategory, - onChanged: (value) { - settingsProvider.groupByCategory = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: - Text(tr('dontShowTrackOnlyWarnings'))), - Switch( - value: - settingsProvider.hideTrackOnlyWarning, - onChanged: (value) { - settingsProvider.hideTrackOnlyWarning = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: - Text(tr('dontShowAPKOriginWarnings'))), - Switch( - value: - settingsProvider.hideAPKOriginWarning, - onChanged: (value) { - settingsProvider.hideAPKOriginWarning = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text(tr('disablePageTransitions'))), - Switch( - value: - settingsProvider.disablePageTransitions, - onChanged: (value) { - settingsProvider.disablePageTransitions = - value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text(tr('reversePageTransitions'))), - Switch( - value: - settingsProvider.reversePageTransitions, - onChanged: settingsProvider - .disablePageTransitions - ? null - : (value) { - settingsProvider - .reversePageTransitions = value; - }) - ], - ), - height16, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text(tr('highlightTouchTargets'))), - Switch( - value: - settingsProvider.highlightTouchTargets, - onChanged: (value) { - settingsProvider.highlightTouchTargets = - value; - }) - ], - ), - height32, - Text( - tr('categories'), - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary), - ), - height16, - const CategoryEditorSelector( - showLabelWhenNotEmpty: false, - ) ], - ))), + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('checkUpdateOnDetailPage')), + ), + Switch( + value: settingsProvider.checkUpdateOnDetailPage, + onChanged: (value) { + settingsProvider.checkUpdateOnDetailPage = + value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + tr('onlyCheckInstalledOrTrackOnlyApps'), + ), + ), + Switch( + value: settingsProvider + .onlyCheckInstalledOrTrackOnlyApps, + onChanged: (value) { + settingsProvider + .onlyCheckInstalledOrTrackOnlyApps = + value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('removeOnExternalUninstall')), + ), + Switch( + value: settingsProvider.removeOnExternalUninstall, + onChanged: (value) { + settingsProvider.removeOnExternalUninstall = + value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('parallelDownloads'))), + Switch( + value: settingsProvider.parallelDownloads, + onChanged: (value) { + settingsProvider.parallelDownloads = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tr('beforeNewInstallsShareToAppVerifier'), + ), + GestureDetector( + onTap: () { + launchUrlString( + 'https://github.com/soupslurpr/AppVerifier', + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + tr('about'), + style: const TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), + ), + ), + ], + ), + ), + Switch( + value: settingsProvider + .beforeNewInstallsShareToAppVerifier, + onChanged: (value) { + settingsProvider + .beforeNewInstallsShareToAppVerifier = + value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('useShizuku'))), + Switch( + value: settingsProvider.useShizuku, + onChanged: (useShizuku) { + if (useShizuku) { + ShizukuApkInstaller.checkPermission().then(( + resCode, + ) { + settingsProvider.useShizuku = resCode! + .startsWith('granted'); + switch (resCode) { + case 'binder_not_found': + showError( + ObtainiumError( + tr('shizukuBinderNotFound'), + ), + context, + ); + case 'old_shizuku': + showError( + ObtainiumError(tr('shizukuOld')), + context, + ); + case 'old_android_with_adb': + showError( + ObtainiumError( + tr('shizukuOldAndroidWithADB'), + ), + context, + ); + case 'denied': + showError( + ObtainiumError(tr('cancelled')), + context, + ); + } + }); + } else { + settingsProvider.useShizuku = false; + } + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('shizukuPretendToBeGooglePlay')), + ), + Switch( + value: + settingsProvider.shizukuPretendToBeGooglePlay, + onChanged: (value) { + settingsProvider.shizukuPretendToBeGooglePlay = + value; + }, + ), + ], + ), + height32, + Text( + tr('sourceSpecific'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ...sourceSpecificFields, + height32, + Text( + tr('appearance'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + DropdownButtonFormField( + decoration: InputDecoration(labelText: tr('theme')), + value: settingsProvider.theme, + items: [ + DropdownMenuItem( + value: ThemeSettings.system, + child: Text(tr('followSystem')), + ), + DropdownMenuItem( + value: ThemeSettings.light, + child: Text(tr('light')), + ), + DropdownMenuItem( + value: ThemeSettings.dark, + child: Text(tr('dark')), + ), + ], + onChanged: (value) { + if (value != null) { + settingsProvider.theme = value; + } + }, + ), + height8, + if (settingsProvider.theme == ThemeSettings.system) + followSystemThemeExplanation, + height16, + if (settingsProvider.theme != ThemeSettings.light) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('useBlackTheme'))), + Switch( + value: settingsProvider.useBlackTheme, + onChanged: (value) { + settingsProvider.useBlackTheme = value; + }, + ), + ], + ), + height8, + useMaterialThemeSwitch, + if (!settingsProvider.useMaterialYou) colorPicker, + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: sortDropdown), + const SizedBox(width: 16), + Expanded(child: orderDropdown), + ], + ), + height16, + localeDropdown, + FutureBuilder( + builder: (ctx, val) { + return (val.data?.version.sdkInt ?? 0) >= 34 + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + height16, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('useSystemFont')), + ), + Switch( + value: + settingsProvider.useSystemFont, + onChanged: (useSystemFont) { + if (useSystemFont) { + NativeFeatures.loadSystemFont() + .then((val) { + settingsProvider + .useSystemFont = + true; + }); + } else { + settingsProvider.useSystemFont = + false; + } + }, + ), + ], + ), + ], + ) + : const SizedBox.shrink(); + }, + future: DeviceInfoPlugin().androidInfo, + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('showWebInAppView'))), + Switch( + value: settingsProvider.showAppWebpage, + onChanged: (value) { + settingsProvider.showAppWebpage = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('pinUpdates'))), + Switch( + value: settingsProvider.pinUpdates, + onChanged: (value) { + settingsProvider.pinUpdates = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('moveNonInstalledAppsToBottom')), + ), + Switch( + value: settingsProvider.buryNonInstalled, + onChanged: (value) { + settingsProvider.buryNonInstalled = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('groupByCategory'))), + Switch( + value: settingsProvider.groupByCategory, + onChanged: (value) { + settingsProvider.groupByCategory = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('dontShowTrackOnlyWarnings')), + ), + Switch( + value: settingsProvider.hideTrackOnlyWarning, + onChanged: (value) { + settingsProvider.hideTrackOnlyWarning = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(tr('dontShowAPKOriginWarnings')), + ), + Switch( + value: settingsProvider.hideAPKOriginWarning, + onChanged: (value) { + settingsProvider.hideAPKOriginWarning = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('disablePageTransitions'))), + Switch( + value: settingsProvider.disablePageTransitions, + onChanged: (value) { + settingsProvider.disablePageTransitions = value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('reversePageTransitions'))), + Switch( + value: settingsProvider.reversePageTransitions, + onChanged: settingsProvider.disablePageTransitions + ? null + : (value) { + settingsProvider.reversePageTransitions = + value; + }, + ), + ], + ), + height16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(tr('highlightTouchTargets'))), + Switch( + value: settingsProvider.highlightTouchTargets, + onChanged: (value) { + settingsProvider.highlightTouchTargets = value; + }, + ), + ], + ), + height32, + Text( + tr('categories'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + height16, + const CategoryEditorSelector( + showLabelWhenNotEmpty: false, + ), + ], + ), + ), + ), SliverToBoxAdapter( child: Column( children: [ - const Divider( - height: 32, - ), + const Divider(height: 32), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( onPressed: () { - launchUrlString(settingsProvider.sourceUrl, - mode: LaunchMode.externalApplication); + launchUrlString( + settingsProvider.sourceUrl, + mode: LaunchMode.externalApplication, + ); }, icon: const Icon(Icons.code), tooltip: tr('appSource'), ), IconButton( onPressed: () { - launchUrlString('https://wiki.obtainium.imranr.dev/', - mode: LaunchMode.externalApplication); + launchUrlString( + 'https://wiki.obtainium.imranr.dev/', + mode: LaunchMode.externalApplication, + ); }, icon: const Icon(Icons.help_outline_rounded), tooltip: tr('wiki'), ), IconButton( onPressed: () { - launchUrlString('https://apps.obtainium.imranr.dev/', - mode: LaunchMode.externalApplication); + launchUrlString( + 'https://apps.obtainium.imranr.dev/', + mode: LaunchMode.externalApplication, + ); }, icon: const Icon(Icons.apps_rounded), tooltip: tr('crowdsourcedConfigsLabel'), ), IconButton( - onPressed: () { - context.read().get().then((logs) { - if (logs.isEmpty) { - showMessage( - ObtainiumError(tr('noLogs')), context); - } else { - showDialog( - context: context, - builder: (BuildContext ctx) { - return const LogsDialog(); - }); - } - }); - }, - icon: const Icon(Icons.bug_report_outlined), - tooltip: tr('appLogs')) + onPressed: () { + context.read().get().then((logs) { + if (logs.isEmpty) { + showMessage(ObtainiumError(tr('noLogs')), context); + } else { + showDialog( + context: context, + builder: (BuildContext ctx) { + return const LogsDialog(); + }, + ); + } + }); + }, + icon: const Icon(Icons.bug_report_outlined), + tooltip: tr('appLogs'), + ), ], ), - const SizedBox( - height: 16, - ) + const SizedBox(height: 16), ], ), - ) - ])); + ), + ], + ), + ); } } @@ -947,11 +995,11 @@ class _LogsDialogState extends State { 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 : tr('noLogs'); - }); - }); + setState(() { + String l = value.map((e) => e.toString()).join('\n\n'); + logString = l.isNotEmpty ? l : tr('noLogs'); + }); + }); } if (logString == null) { @@ -964,53 +1012,57 @@ class _LogsDialogState extends State { content: Column( children: [ DropdownButtonFormField( - value: days.first, - items: days - .map((e) => DropdownMenuItem( - value: e, - child: Text(plural('day', e)), - )) - .toList(), - onChanged: (d) { - filterLogs(d ?? 7); - }), - const SizedBox( - height: 32, + value: days.first, + items: days + .map( + (e) => + DropdownMenuItem(value: e, child: Text(plural('day', e))), + ) + .toList(), + onChanged: (d) { + filterLogs(d ?? 7); + }, ), - Text(logString ?? '') + const SizedBox(height: 32), + Text(logString ?? ''), ], ), actions: [ TextButton( - onPressed: () async { - var cont = (await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('appLogs'), - items: const [], - initValid: true, - message: tr('removeFromObtainium'), - ); - })) != - null; - if (cont) { - logsProvider.clear(); - Navigator.of(context).pop(); - } - }, - child: Text(tr('remove'))), - TextButton( - onPressed: () { + onPressed: () async { + var cont = + (await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('appLogs'), + items: const [], + initValid: true, + message: tr('removeFromObtainium'), + ); + }, + )) != + null; + if (cont) { + logsProvider.clear(); Navigator.of(context).pop(); - }, - child: Text(tr('close'))), + } + }, + child: Text(tr('remove')), + ), TextButton( - onPressed: () { - Share.share(logString ?? '', subject: tr('appLogs')); - Navigator.of(context).pop(); - }, - child: Text(tr('share'))) + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('close')), + ), + TextButton( + onPressed: () { + Share.share(logString ?? '', subject: tr('appLogs')); + Navigator.of(context).pop(); + }, + child: Text(tr('share')), + ), ], ); } @@ -1022,13 +1074,14 @@ class CategoryEditorSelector extends StatefulWidget { final Set preselected; final WrapAlignment alignment; final bool showLabelWhenNotEmpty; - const CategoryEditorSelector( - {super.key, - this.onSelected, - this.singleSelect = false, - this.preselected = const {}, - this.alignment = WrapAlignment.start, - this.showLabelWhenNotEmpty = true}); + const CategoryEditorSelector({ + super.key, + this.onSelected, + this.singleSelect = false, + this.preselected = const {}, + this.alignment = WrapAlignment.start, + this.showLabelWhenNotEmpty = true, + }); @override State createState() => _CategoryEditorSelectorState(); @@ -1041,38 +1094,48 @@ class _CategoryEditorSelectorState extends State { Widget build(BuildContext context) { var settingsProvider = context.watch(); var appsProvider = context.watch(); - storedValues = settingsProvider.categories.map((key, value) => MapEntry( + storedValues = settingsProvider.categories.map( + (key, value) => MapEntry( key, - MapEntry(value, - storedValues[key]?.value ?? widget.preselected.contains(key)))); + MapEntry( + value, + storedValues[key]?.value ?? widget.preselected.contains(key), + ), + ), + ); return GeneratedForm( - items: [ - [ - GeneratedFormTagInput('categories', - label: tr('categories'), - emptyMessage: tr('noCategories'), - defaultValue: storedValues, - alignment: widget.alignment, - deleteConfirmationMessage: MapEntry( - tr('deleteCategoriesQuestion'), - tr('categoryDeleteWarning')), - singleSelect: widget.singleSelect, - showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty) - ] + items: [ + [ + GeneratedFormTagInput( + 'categories', + label: tr('categories'), + emptyMessage: tr('noCategories'), + defaultValue: storedValues, + alignment: widget.alignment, + deleteConfirmationMessage: MapEntry( + tr('deleteCategoriesQuestion'), + tr('categoryDeleteWarning'), + ), + singleSelect: widget.singleSelect, + showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty, + ), ], - onValueChanges: ((values, valid, isBuilding) { - if (!isBuilding) { - storedValues = - values['categories'] as Map>; - settingsProvider.setCategories( - storedValues.map((key, value) => MapEntry(key, value.key)), - appsProvider: appsProvider); - if (widget.onSelected != null) { - widget.onSelected!(storedValues.keys - .where((k) => storedValues[k]!.value) - .toList()); - } + ], + onValueChanges: ((values, valid, isBuilding) { + if (!isBuilding) { + storedValues = + values['categories'] as Map>; + settingsProvider.setCategories( + storedValues.map((key, value) => MapEntry(key, value.key)), + appsProvider: appsProvider, + ); + if (widget.onSelected != null) { + widget.onSelected!( + storedValues.keys.where((k) => storedValues[k]!.value).toList(), + ); } - })); + } + }), + ); } } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index ad506f0..769be36 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -74,7 +74,7 @@ List generateStandardVersionRegExStrings() { '[0-9]+', '[0-9]+\\.[0-9]+', '[0-9]+\\.[0-9]+\\.[0-9]+', - '[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+' + '[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+', ]; var preSuffixes = ['-', '\\+']; var suffixes = ['alpha', 'beta', 'ose', '[0-9]+']; @@ -103,8 +103,9 @@ Set findStandardFormatsForVersion(String version, bool strict) { // If !strict, even a substring match is valid Set results = {}; for (var pattern in standardVersionRegExStrings) { - if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}') - .hasMatch(version)) { + if (RegExp( + '${strict ? '^' : ''}$pattern${strict ? '\$' : ''}', + ).hasMatch(version)) { results.add(pattern); } } @@ -127,8 +128,10 @@ moveStrToEnd(List arr, String str, {String? strB}) { } List> moveStrToEndMapEntryWithCount( - List> arr, MapEntry str, - {MapEntry? strB}) { + List> arr, + MapEntry str, { + MapEntry? strB, +}) { MapEntry? temp; arr.removeWhere((element) { bool resA = element.key == str.key; @@ -146,30 +149,45 @@ List> moveStrToEndMapEntryWithCount( return arr; } -Future downloadFileWithRetry(String url, String fileName, - bool fileNameHasExt, Function? onProgress, String destDir, - {bool useExisting = true, - Map? headers, - int retries = 3, - bool allowInsecure = false, - LogsProvider? logs}) async { +Future downloadFileWithRetry( + String url, + String fileName, + bool fileNameHasExt, + Function? onProgress, + String destDir, { + bool useExisting = true, + Map? headers, + int retries = 3, + bool allowInsecure = false, + LogsProvider? logs, +}) async { try { return await downloadFile( - url, fileName, fileNameHasExt, onProgress, destDir, - useExisting: useExisting, - headers: headers, - allowInsecure: allowInsecure, - logs: logs); + url, + fileName, + fileNameHasExt, + onProgress, + destDir, + useExisting: useExisting, + headers: headers, + allowInsecure: allowInsecure, + logs: logs, + ); } catch (e) { if (retries > 0 && e is ClientException) { await Future.delayed(const Duration(seconds: 5)); return await downloadFileWithRetry( - url, fileName, fileNameHasExt, onProgress, destDir, - useExisting: useExisting, - headers: headers, - retries: (retries - 1), - allowInsecure: allowInsecure, - logs: logs); + url, + fileName, + fileNameHasExt, + onProgress, + destDir, + useExisting: useExisting, + headers: headers, + retries: (retries - 1), + allowInsecure: allowInsecure, + logs: logs, + ); } else { rethrow; } @@ -183,17 +201,27 @@ String hashListOfLists(List> data) { return hash.hashCode.toString(); } -Future checkPartialDownloadHashDynamic(String url, - {int startingSize = 1024, - int lowerLimit = 128, - Map? headers, - bool allowInsecure = false}) async { +Future checkPartialDownloadHashDynamic( + String url, { + int startingSize = 1024, + int lowerLimit = 128, + Map? headers, + bool allowInsecure = false, +}) async { for (int i = startingSize; i >= lowerLimit; i -= 256) { List ab = await Future.wait([ - checkPartialDownloadHash(url, i, - headers: headers, allowInsecure: allowInsecure), - checkPartialDownloadHash(url, i, - headers: headers, allowInsecure: allowInsecure) + checkPartialDownloadHash( + url, + i, + headers: headers, + allowInsecure: allowInsecure, + ), + checkPartialDownloadHash( + url, + i, + headers: headers, + allowInsecure: allowInsecure, + ), ]); if (ab[0] == ab[1]) { return ab[0]; @@ -202,8 +230,12 @@ Future checkPartialDownloadHashDynamic(String url, throw NoVersionError(); } -Future checkPartialDownloadHash(String url, int bytesToGrab, - {Map? headers, bool allowInsecure = false}) async { +Future checkPartialDownloadHash( + String url, + int bytesToGrab, { + Map? headers, + bool allowInsecure = false, +}) async { var req = Request('GET', Uri.parse(url)); if (headers != null) { req.headers.addAll(headers); @@ -218,8 +250,11 @@ Future checkPartialDownloadHash(String url, int bytesToGrab, return hashListOfLists(bytes); } -Future checkETagHeader(String url, - {Map? headers, bool allowInsecure = false}) async { +Future checkETagHeader( + String url, { + Map? headers, + bool allowInsecure = false, +}) 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)); @@ -238,16 +273,23 @@ deleteFile(File file) { try { file.deleteSync(recursive: true); } on PathAccessException catch (e) { - throw ObtainiumError(tr('fileDeletionError', args: [e.path ?? tr('unknown')])); + throw ObtainiumError( + tr('fileDeletionError', args: [e.path ?? tr('unknown')]), + ); } } -Future downloadFile(String url, String fileName, bool fileNameHasExt, - Function? onProgress, String destDir, - {bool useExisting = true, - Map? headers, - bool allowInsecure = false, - LogsProvider? logs}) async { +Future downloadFile( + String url, + String fileName, + bool fileNameHasExt, + Function? onProgress, + String destDir, { + bool useExisting = true, + Map? headers, + 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)); @@ -311,7 +353,8 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, bool tempFileExists = tempDownloadedFile.existsSync(); if (tempFileExists && useExisting) { logs?.add( - 'Partial download exists - will wait: ${tempDownloadedFile.uri.pathSegments.last}'); + 'Partial download exists - will wait: ${tempDownloadedFile.uri.pathSegments.last}', + ); bool isDownloading = true; int currentTempFileSize = await tempDownloadedFile.length(); bool shouldReturn = false; @@ -322,10 +365,12 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, if (newTempFileSize > currentTempFileSize) { currentTempFileSize = newTempFileSize; logs?.add( - 'Existing partial download still in progress: ${tempDownloadedFile.uri.pathSegments.last}'); + 'Existing partial download still in progress: ${tempDownloadedFile.uri.pathSegments.last}', + ); } else { logs?.add( - 'Ignoring existing partial download: ${tempDownloadedFile.uri.pathSegments.last}'); + 'Ignoring existing partial download: ${tempDownloadedFile.uri.pathSegments.last}', + ); break; } } else { @@ -334,11 +379,13 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, } if (shouldReturn) { logs?.add( - 'Existing partial download completed - not repeating: ${tempDownloadedFile.uri.pathSegments.last}'); + 'Existing partial download completed - not repeating: ${tempDownloadedFile.uri.pathSegments.last}', + ); return downloadedFile; } else { logs?.add( - 'Existing partial download not in progress: ${tempDownloadedFile.uri.pathSegments.last}'); + 'Existing partial download not in progress: ${tempDownloadedFile.uri.pathSegments.last}', + ); } } @@ -358,8 +405,12 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, } else if (tempDownloadedFile.existsSync()) { deleteFile(tempDownloadedFile); } - var responseWithClient = - await sourceRequestStreamResponse('GET', url, reqHeaders, {}); + var responseWithClient = await sourceRequestStreamResponse( + 'GET', + url, + reqHeaders, + {}, + ); HttpClient responseClient = responseWithClient.value.key; HttpClientResponse response = responseWithClient.value.value; sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly); @@ -391,20 +442,22 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, } return chunk; }) - .transform(StreamTransformer, List>.fromHandlers( - handleData: (List data, EventSink> s) { - downloadBuffer.add(data); - if (downloadBuffer.length >= downloadBufferSize) { - s.add(downloadBuffer.takeBytes()); - } - }, - handleDone: (EventSink> s) { - if (downloadBuffer.isNotEmpty) { - s.add(downloadBuffer.takeBytes()); - } - s.close(); - }, - )) + .transform( + StreamTransformer, List>.fromHandlers( + handleData: (List data, EventSink> s) { + downloadBuffer.add(data); + if (downloadBuffer.length >= downloadBufferSize) { + s.add(downloadBuffer.takeBytes()); + } + }, + handleDone: (EventSink> s) { + if (downloadBuffer.isNotEmpty) { + s.add(downloadBuffer.takeBytes()); + } + s.close(); + }, + ), + ) .pipe(sink); await sink.close(); progress = null; @@ -426,8 +479,10 @@ Future> getAllInstalledInfo() async { return await pm.getInstalledPackages() ?? []; } -Future getInstalledInfo(String? packageName, - {bool printErr = true}) async { +Future getInstalledInfo( + String? packageName, { + bool printErr = true, +}) async { if (packageName != null) { try { return await pm.getPackageInfo(packageName: packageName); @@ -476,13 +531,15 @@ class AppsProvider with ChangeNotifier { iconsCacheDir.createSync(); } } else { - APKDir = - Directory('${(await getExternalStorageDirectory())!.path}/apks'); + APKDir = Directory( + '${(await getExternalStorageDirectory())!.path}/apks', + ); if (!APKDir.existsSync()) { APKDir.createSync(); } - iconsCacheDir = - Directory('${(await getExternalStorageDirectory())!.path}/icons'); + iconsCacheDir = Directory( + '${(await getExternalStorageDirectory())!.path}/icons', + ); if (!iconsCacheDir.existsSync()) { iconsCacheDir.createSync(); } @@ -495,16 +552,20 @@ class AppsProvider with ChangeNotifier { APKDir.listSync() .where((element) => element.statSync().modified.isBefore(cutoff)) .forEach((partialApk) { - if (!areDownloadsRunning()) { - partialApk.delete(recursive: true); - } - }); + if (!areDownloadsRunning()) { + partialApk.delete(recursive: true); + } + }); } }(); } - Future handleAPKIDChange(App app, PackageInfo newInfo, - File downloadedFile, String downloadUrl) async { + Future handleAPKIDChange( + App app, + PackageInfo newInfo, + File downloadedFile, + String downloadUrl, + ) async { // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed // The former case should be handled (give the App its real ID), the latter is a security issue var isTempIdBool = isTempId(app); @@ -517,31 +578,39 @@ class AppsProvider with ChangeNotifier { var originalAppId = app.id; app.id = newInfo.packageName!; downloadedFile = downloadedFile.renameSync( - '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); + '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}', + ); if (apps[originalAppId] != null) { await removeApps([originalAppId]); - await saveApps([app], - onlyIfExists: !isTempIdBool && !idChangeWasAllowed); + await saveApps([ + app, + ], onlyIfExists: !isTempIdBool && !idChangeWasAllowed); } } return downloadedFile; } - Future downloadApp(App app, BuildContext? context, - {NotificationsProvider? notificationsProvider, - bool useExisting = true}) async { + Future downloadApp( + App app, + BuildContext? context, { + NotificationsProvider? notificationsProvider, + bool useExisting = true, + }) async { var notifId = DownloadNotification(app.finalName, 0).id; if (apps[app.id] != null) { apps[app.id]!.downloadProgress = 0; notifyListeners(); } try { - AppSource source = SourceProvider() - .getSource(app.url, overrideSource: app.overrideSource); + AppSource source = SourceProvider().getSource( + app.url, + overrideSource: app.overrideSource, + ); String downloadUrl = await source.apkUrlPrefetchModifier( - app.apkUrls[app.preferredApkIndex].value, - app.url, - app.additionalSettings); + app.apkUrls[app.preferredApkIndex].value, + app.url, + app.additionalSettings, + ); var notif = DownloadNotification(app.finalName, 100); notificationsProvider?.cancel(notif.id); int? prevProg; @@ -550,25 +619,32 @@ class AppsProvider with ChangeNotifier { fileNameNoExt = '$fileNameNoExt.${app.apkUrls[app.preferredApkIndex].key.split('.').last}'; } - var headers = await source.getRequestHeaders(app.additionalSettings, - forAPKDownload: true); + var headers = await source.getRequestHeaders( + app.additionalSettings, + forAPKDownload: true, + ); var downloadedFile = await downloadFileWithRetry( - downloadUrl, fileNameNoExt, source.urlsAlwaysHaveExtension, - headers: headers, (double? progress) { - int? prog = progress?.ceil(); - if (apps[app.id] != null) { - apps[app.id]!.downloadProgress = progress; - notifyListeners(); - } - notif = DownloadNotification(app.finalName, prog ?? 100); - if (prog != null && prevProg != prog) { - notificationsProvider?.notify(notif); - } - prevProg = prog; - }, APKDir.path, - useExisting: useExisting, - allowInsecure: app.additionalSettings['allowInsecure'] == true, - logs: logs); + downloadUrl, + fileNameNoExt, + source.urlsAlwaysHaveExtension, + headers: headers, + (double? progress) { + int? prog = progress?.ceil(); + if (apps[app.id] != null) { + apps[app.id]!.downloadProgress = progress; + notifyListeners(); + } + notif = DownloadNotification(app.finalName, prog ?? 100); + if (prog != null && prevProg != prog) { + notificationsProvider?.notify(notif); + } + prevProg = prog; + }, + APKDir.path, + useExisting: useExisting, + 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; @@ -581,7 +657,8 @@ class AppsProvider with ChangeNotifier { Directory? xapkDir; if (isAPK) { newInfo = await pm.getPackageArchiveInfo( - archiveFilePath: downloadedFile.path); + archiveFilePath: downloadedFile.path, + ); } else { // Assume XAPK String xapkDirPath = '${downloadedFile.path}-dir'; @@ -601,16 +678,14 @@ class AppsProvider with ChangeNotifier { return res; }); if (temp != null) { - apks = [ - temp!, - ...apks, - ]; + apks = [temp!, ...apks]; } for (var i = 0; i < apks.length; i++) { try { - newInfo = - await pm.getPackageArchiveInfo(archiveFilePath: apks[i].path); + newInfo = await pm.getPackageArchiveInfo( + archiveFilePath: apks[i].path, + ); if (newInfo != null) { break; } @@ -625,8 +700,12 @@ class AppsProvider with ChangeNotifier { downloadedFile.delete(); throw ObtainiumError('Could not get ID from APK'); } - downloadedFile = - await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); + downloadedFile = await handleAPKIDChange( + app, + newInfo, + downloadedFile, + downloadUrl, + ); // Delete older versions of the file if any for (var file in downloadedFile.parent.listSync()) { var fn = file.path.split('/').last; @@ -671,17 +750,20 @@ class AppsProvider with ChangeNotifier { String? installerPackageName; try { installerPackageName = osInfo.version.sdkInt >= 30 - ? (await pm.getInstallSourceInfo(packageName: app.id)) - ?.installingPackageName + ? (await pm.getInstallSourceInfo( + packageName: app.id, + ))?.installingPackageName : (await pm.getInstallerPackageName(packageName: app.id)); } catch (e) { logs.add( - 'Failed to get installed package details: ${app.id} (${e.toString()})'); + 'Failed to get installed package details: ${app.id} (${e.toString()})', + ); return false; // App probably not installed } - int? targetSDK = - (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion; + int? targetSDK = (await getInstalledInfo( + app.id, + ))?.applicationInfo?.targetSdkVersion; // The APK should target a new enough API // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) if (!(targetSDK != null && targetSDK >= (osInfo.version.sdkInt - 3))) { @@ -709,11 +791,13 @@ class AppsProvider with ChangeNotifier { } Future waitForUserToReturnToForeground(BuildContext context) async { - NotificationsProvider notificationsProvider = - context.read(); + NotificationsProvider notificationsProvider = context + .read(); if (!isForeground) { - await notificationsProvider.notify(completeInstallationNotification, - cancelExisting: true); + await notificationsProvider.notify( + completeInstallationNotification, + cancelExisting: true, + ); while (await FGBGEvents.instance.stream.first != FGBGType.foreground) {} await notificationsProvider.cancel(completeInstallationNotification.id); } @@ -724,13 +808,17 @@ class AppsProvider with ChangeNotifier { Future unzipFile(String filePath, String destinationPath) async { await ZipFile.extractToDirectory( - zipFile: File(filePath), destinationDir: Directory(destinationPath)); + zipFile: File(filePath), + destinationDir: Directory(destinationPath), + ); } Future installXApkDir( - DownloadedXApkDir dir, BuildContext? firstTimeWithContext, - {bool needsBGWorkaround = false, - bool shizukuPretendToBeGooglePlay = false}) async { + DownloadedXApkDir dir, + BuildContext? firstTimeWithContext, { + bool needsBGWorkaround = false, + bool shizukuPretendToBeGooglePlay = false, + }) async { // We don't know which APKs in an XAPK are supported by the user's device // So we try installing all of them and assume success if at least one installed // If 0 APKs installed, throw the first install error encountered @@ -738,9 +826,10 @@ class AppsProvider with ChangeNotifier { try { MultiAppMultiError errors = MultiAppMultiError(); List APKFiles = []; - for (var file in dir.extracted - .listSync(recursive: true, followLinks: false) - .whereType()) { + for (var file + in dir.extracted + .listSync(recursive: true, followLinks: false) + .whereType()) { if (file.path.toLowerCase().endsWith('.apk')) { APKFiles.add(file); } else if (file.path.toLowerCase().endsWith('.obb')) { @@ -757,20 +846,19 @@ class AppsProvider with ChangeNotifier { return res; }); if (temp != null) { - APKFiles = [ - temp!, - ...APKFiles, - ]; + APKFiles = [temp!, ...APKFiles]; } try { await installApk( - DownloadedApk(dir.appId, APKFiles[0]), firstTimeWithContext, - needsBGWorkaround: needsBGWorkaround, - shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay, - additionalAPKs: APKFiles.sublist(1) - .map((a) => DownloadedApk(dir.appId, a)) - .toList()); + DownloadedApk(dir.appId, APKFiles[0]), + firstTimeWithContext, + needsBGWorkaround: needsBGWorkaround, + shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay, + additionalAPKs: APKFiles.sublist( + 1, + ).map((a) => DownloadedApk(dir.appId, a)).toList(), + ); somethingInstalled = true; dir.file.delete(recursive: true); } catch (e) { @@ -787,22 +875,28 @@ class AppsProvider with ChangeNotifier { } Future installApk( - DownloadedApk file, BuildContext? firstTimeWithContext, - {bool needsBGWorkaround = false, - bool shizukuPretendToBeGooglePlay = false, - List additionalAPKs = const []}) async { + DownloadedApk file, + BuildContext? firstTimeWithContext, { + bool needsBGWorkaround = false, + bool shizukuPretendToBeGooglePlay = false, + List additionalAPKs = const [], + }) async { if (firstTimeWithContext != null && settingsProvider.beforeNewInstallsShareToAppVerifier && (await getInstalledInfo('dev.soupslurpr.appverifier')) != null) { - XFile f = XFile.fromData(file.file.readAsBytesSync(), - mimeType: 'application/vnd.android.package-archive'); + XFile f = XFile.fromData( + file.file.readAsBytesSync(), + mimeType: 'application/vnd.android.package-archive', + ); Fluttertoast.showToast( - msg: tr('appVerifierInstructionToast'), - toastLength: Toast.LENGTH_LONG); + msg: tr('appVerifierInstructionToast'), + toastLength: Toast.LENGTH_LONG, + ); await Share.shareXFiles([f]); } - var newInfo = - await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); + var newInfo = await pm.getPackageArchiveInfo( + archiveFilePath: file.file.path, + ); if (newInfo == null) { try { deleteFile(file.file); @@ -817,7 +911,8 @@ class AppsProvider with ChangeNotifier { } PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); logs.add( - 'Installing "${newInfo.packageName}" version "${newInfo.versionName}" versionCode "${newInfo.versionCode}"${appInfo != null ? ' (from existing version "${appInfo.versionName}" versionCode "${appInfo.versionCode}")' : ''}'); + 'Installing "${newInfo.packageName}" version "${newInfo.versionName}" versionCode "${newInfo.versionCode}"${appInfo != null ? ' (from existing version "${appInfo.versionName}" versionCode "${appInfo.versionCode}")' : ''}', + ); if (appInfo != null && newInfo.versionCode! < appInfo.versionCode! && !(await canDowngradeApps())) { @@ -831,18 +926,22 @@ class AppsProvider with ChangeNotifier { // TODO: When fixed, update this function and the calls to it accordingly apps[file.appId]!.app.installedVersion = apps[file.appId]!.app.latestVersion; - await saveApps([apps[file.appId]!.app], - attemptToCorrectInstallStatus: false); + await saveApps([ + apps[file.appId]!.app, + ], attemptToCorrectInstallStatus: false); } int? code; if (!settingsProvider.useShizuku) { var allAPKs = [file.file.path]; allAPKs.addAll(additionalAPKs.map((a) => a.file.path)); code = await AndroidPackageInstaller.installApk( - apkFilePath: allAPKs.join(',')); + apkFilePath: allAPKs.join(','), + ); } else { - code = await ShizukuApkInstaller.installAPK(file.file.uri.toString(), - shizukuPretendToBeGooglePlay ? "com.android.vending" : ""); + code = await ShizukuApkInstaller.installAPK( + file.file.uri.toString(), + shizukuPretendToBeGooglePlay ? "com.android.vending" : "", + ); } bool installed = false; if (code != null && code != 0 && code != 3) { @@ -884,39 +983,46 @@ class AppsProvider with ChangeNotifier { void uninstallApp(String appId) async { var intent = AndroidIntent( - action: 'android.intent.action.DELETE', - data: 'package:$appId', - flags: [Flag.FLAG_ACTIVITY_NEW_TASK], - package: 'vnd.android.package-archive'); + action: 'android.intent.action.DELETE', + data: 'package:$appId', + flags: [Flag.FLAG_ACTIVITY_NEW_TASK], + package: 'vnd.android.package-archive', + ); await intent.launch(); } Future?> confirmAppFileUrl( - App app, BuildContext? context, bool pickAnyAsset, - {bool evenIfSingleChoice = false}) async { + App app, + BuildContext? context, + bool pickAnyAsset, { + bool evenIfSingleChoice = false, + }) async { var urlsToSelectFrom = app.apkUrls; if (pickAnyAsset) { urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls]; } // If the App has more than one APK, the user should pick one (if context provided) - MapEntry? appFileUrl = urlsToSelectFrom[ - app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0]; + MapEntry? appFileUrl = + urlsToSelectFrom[app.preferredApkIndex >= 0 + ? app.preferredApkIndex + : 0]; // get device supported architecture List archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; if ((urlsToSelectFrom.length > 1 || evenIfSingleChoice) && context != null) { appFileUrl = await showDialog( - // ignore: use_build_context_synchronously - context: context, - builder: (BuildContext ctx) { - return AppFilePicker( - app: app, - initVal: appFileUrl, - archs: archs, - pickAnyAsset: pickAnyAsset, - ); - }); + // ignore: use_build_context_synchronously + context: context, + builder: (BuildContext ctx) { + return AppFilePicker( + app: app, + initVal: appFileUrl, + archs: archs, + pickAnyAsset: pickAnyAsset, + ); + }, + ); } getHost(String url) { var temp = Uri.parse(url).host.split('.'); @@ -929,12 +1035,15 @@ class AppsProvider with ChangeNotifier { context != null) { if (!(settingsProvider.hideAPKOriginWarning) && await showDialog( - // ignore: use_build_context_synchronously - context: context, - builder: (BuildContext ctx) { - return APKOriginWarningDialog( - sourceUrl: app.url, apkUrl: appFileUrl!.value); - }) != + // ignore: use_build_context_synchronously + context: context, + builder: (BuildContext ctx) { + return APKOriginWarningDialog( + sourceUrl: app.url, + apkUrl: appFileUrl!.value, + ); + }, + ) != true) { appFileUrl = null; } @@ -948,10 +1057,12 @@ class AppsProvider with ChangeNotifier { // If user input is needed and the App is in the background, a notification is sent to get the user's attention // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result Future> downloadAndInstallLatestApps( - List appIds, BuildContext? context, - {NotificationsProvider? notificationsProvider, - bool forceParallelDownloads = false, - bool useExisting = true}) async { + List appIds, + BuildContext? context, { + NotificationsProvider? notificationsProvider, + bool forceParallelDownloads = false, + bool useExisting = true, + }) async { notificationsProvider = notificationsProvider ?? context?.read(); List appsToInstall = []; @@ -975,9 +1086,7 @@ class AppsProvider with ChangeNotifier { apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false); } if (apkUrl != null) { - int urlInd = apps[id]! - .app - .apkUrls + int urlInd = apps[id]!.app.apkUrls .map((e) => e.value) .toList() .indexOf(apkUrl.value); @@ -994,66 +1103,91 @@ class AppsProvider with ChangeNotifier { } } // Mark all specified track-only apps as latest - saveApps(trackOnlyAppsToUpdate.map((e) { - var a = apps[e]!.app; - a.installedVersion = a.latestVersion; - return a; - }).toList()); + saveApps( + trackOnlyAppsToUpdate.map((e) { + var a = apps[e]!.app; + a.installedVersion = a.latestVersion; + return a; + }).toList(), + ); // Prepare to download+install Apps MultiAppMultiError errors = MultiAppMultiError(); List installedIds = []; // Move Obtainium to the end of the line (let all other apps update first) - appsToInstall = - moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId); + appsToInstall = moveStrToEnd( + appsToInstall, + obtainiumId, + strB: obtainiumTempId, + ); - Future installFn(String id, bool willBeSilent, - DownloadedApk? downloadedFile, DownloadedXApkDir? downloadedDir) async { + Future installFn( + String id, + bool willBeSilent, + DownloadedApk? downloadedFile, + DownloadedXApkDir? downloadedDir, + ) async { apps[id]?.downloadProgress = -1; notifyListeners(); try { bool sayInstalled = true; - var contextIfNewInstall = - apps[id]?.installedInfo == null ? context : null; + var contextIfNewInstall = apps[id]?.installedInfo == null + ? context + : null; bool needBGWorkaround = willBeSilent && context == null && !settingsProvider.useShizuku; - bool shizukuPretendToBeGooglePlay = settingsProvider - .shizukuPretendToBeGooglePlay || + bool shizukuPretendToBeGooglePlay = + settingsProvider.shizukuPretendToBeGooglePlay || apps[id]!.app.additionalSettings['shizukuPretendToBeGooglePlay'] == true; if (downloadedFile != null) { if (needBGWorkaround) { // ignore: use_build_context_synchronously - installApk(downloadedFile, contextIfNewInstall, - needsBGWorkaround: true, - shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay); + installApk( + downloadedFile, + contextIfNewInstall, + needsBGWorkaround: true, + shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay, + ); } else { // ignore: use_build_context_synchronously - sayInstalled = await installApk(downloadedFile, contextIfNewInstall, - shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay); + sayInstalled = await installApk( + downloadedFile, + contextIfNewInstall, + shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay, + ); } } else { if (needBGWorkaround) { // ignore: use_build_context_synchronously - installXApkDir(downloadedDir!, contextIfNewInstall, - needsBGWorkaround: true); + installXApkDir( + downloadedDir!, + contextIfNewInstall, + needsBGWorkaround: true, + ); } else { // ignore: use_build_context_synchronously sayInstalled = await installXApkDir( - downloadedDir!, contextIfNewInstall, - shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay); + downloadedDir!, + contextIfNewInstall, + shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay, + ); } } if (willBeSilent && context == null) { if (!settingsProvider.useShizuku) { - notificationsProvider?.notify(SilentUpdateAttemptNotification( - [apps[id]!.app], - id: id.hashCode)); + notificationsProvider?.notify( + SilentUpdateAttemptNotification([apps[id]!.app], id: id.hashCode), + ); } else { - notificationsProvider?.notify(SilentUpdateNotification( - [apps[id]!.app], sayInstalled, - id: id.hashCode)); + notificationsProvider?.notify( + SilentUpdateNotification( + [apps[id]!.app], + sayInstalled, + id: id.hashCode, + ), + ); } } if (sayInstalled) { @@ -1065,17 +1199,22 @@ class AppsProvider with ChangeNotifier { } } - Future> downloadFn(String id, - {bool skipInstalls = false}) async { + Future> downloadFn( + String id, { + bool skipInstalls = false, + }) async { bool willBeSilent = false; DownloadedApk? downloadedFile; DownloadedXApkDir? downloadedDir; try { var downloadedArtifact = // ignore: use_build_context_synchronously - await downloadApp(apps[id]!.app, context, - notificationsProvider: notificationsProvider, - useExisting: useExisting); + await downloadApp( + apps[id]!.app, + context, + notificationsProvider: notificationsProvider, + useExisting: useExisting, + ); if (downloadedArtifact is DownloadedApk) { downloadedFile = downloadedArtifact; } else { @@ -1110,7 +1249,7 @@ class AppsProvider with ChangeNotifier { 'id': id, 'willBeSilent': willBeSilent, 'downloadedFile': downloadedFile, - 'downloadedDir': downloadedDir + 'downloadedDir': downloadedDir, }; } @@ -1121,16 +1260,18 @@ class AppsProvider with ChangeNotifier { } } else { downloadResults = await Future.wait( - appsToInstall.map((id) => downloadFn(id, skipInstalls: true))); + appsToInstall.map((id) => downloadFn(id, skipInstalls: true)), + ); } for (var res in downloadResults) { if (!errors.appIdNames.containsKey(res['id'])) { try { await installFn( - res['id'] as String, - res['willBeSilent'] as bool, - res['downloadedFile'] as DownloadedApk?, - res['downloadedDir'] as DownloadedXApkDir?); + res['id'] as String, + res['willBeSilent'] as bool, + res['downloadedFile'] as DownloadedApk?, + res['downloadedDir'] as DownloadedXApkDir?, + ); } catch (e) { var id = res['id'] as String; errors.add(id, e, appName: apps[id]?.name); @@ -1146,10 +1287,12 @@ class AppsProvider with ChangeNotifier { } Future> downloadAppAssets( - List appIds, BuildContext context, - {bool forceParallelDownloads = false}) async { - NotificationsProvider notificationsProvider = - context.read(); + List appIds, + BuildContext context, { + bool forceParallelDownloads = false, + }) async { + NotificationsProvider notificationsProvider = context + .read(); List, App>> filesToDownload = []; for (var id in appIds) { if (apps[id] == null) { @@ -1165,15 +1308,23 @@ class AppsProvider with ChangeNotifier { apps[id]!.app.otherAssetUrls.isNotEmpty) { // ignore: use_build_context_synchronously MapEntry? tempFileUrl = await confirmAppFileUrl( - apps[id]!.app, context, true, - evenIfSingleChoice: true); + apps[id]!.app, + context, + true, + evenIfSingleChoice: true, + ); if (tempFileUrl != null) { fileUrl = MapEntry( - tempFileUrl.key, - await (SourceProvider().getSource(apps[id]!.app.url, - overrideSource: apps[id]!.app.overrideSource)) - .apkUrlPrefetchModifier(tempFileUrl.value, apps[id]!.app.url, - apps[id]!.app.additionalSettings)); + tempFileUrl.key, + await (SourceProvider().getSource( + apps[id]!.app.url, + overrideSource: apps[id]!.app.overrideSource, + )).apkUrlPrefetchModifier( + tempFileUrl.value, + apps[id]!.app.url, + apps[id]!.app.additionalSettings, + ), + ); } } if (fileUrl != null) { @@ -1188,21 +1339,29 @@ class AppsProvider with ChangeNotifier { Future downloadFn(MapEntry fileUrl, App app) async { try { String downloadPath = '${await getStorageRootPath()}/Download'; - await downloadFile(fileUrl.value, fileUrl.key, true, - (double? progress) { - notificationsProvider - .notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0)); - }, downloadPath, - headers: await SourceProvider() - .getSource(app.url, overrideSource: app.overrideSource) - .getRequestHeaders(app.additionalSettings, - forAPKDownload: - fileUrl.key.endsWith('.apk') ? true : false), - useExisting: false, - allowInsecure: app.additionalSettings['allowInsecure'] == true, - logs: logs); - notificationsProvider - .notify(DownloadedNotification(fileUrl.key, fileUrl.value)); + await downloadFile( + fileUrl.value, + fileUrl.key, + true, + (double? progress) { + notificationsProvider.notify( + DownloadNotification(fileUrl.key, progress?.ceil() ?? 0), + ); + }, + downloadPath, + headers: await SourceProvider() + .getSource(app.url, overrideSource: app.overrideSource) + .getRequestHeaders( + app.additionalSettings, + forAPKDownload: fileUrl.key.endsWith('.apk') ? true : false, + ), + useExisting: false, + allowInsecure: app.additionalSettings['allowInsecure'] == true, + logs: logs, + ); + notificationsProvider.notify( + DownloadedNotification(fileUrl.key, fileUrl.value), + ); } catch (e) { errors.add(fileUrl.key, e); } finally { @@ -1215,8 +1374,11 @@ class AppsProvider with ChangeNotifier { await downloadFn(urlWithApp.key, urlWithApp.value); } } else { - await Future.wait(filesToDownload - .map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value))); + await Future.wait( + filesToDownload.map( + (urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value), + ), + ); } if (errors.idsByErrorString.isNotEmpty) { throw errors; @@ -1225,8 +1387,9 @@ class AppsProvider with ChangeNotifier { } Future getAppsDir() async { - Directory appsDir = - Directory('${(await getExternalStorageDirectory())!.path}/app_data'); + Directory appsDir = Directory( + '${(await getExternalStorageDirectory())!.path}/app_data', + ); if (!appsDir.existsSync()) { appsDir.createSync(); } @@ -1237,20 +1400,22 @@ class AppsProvider with ChangeNotifier { if (app?.app == null) { return false; } - var source = SourceProvider() - .getSource(app!.app.url, overrideSource: app.app.overrideSource); + var source = SourceProvider().getSource( + app!.app.url, + overrideSource: app.app.overrideSource, + ); var naiveStandardVersionDetection = app.app.additionalSettings['naiveStandardVersionDetection'] == true || - source.naiveStandardVersionDetection; + source.naiveStandardVersionDetection; String? realInstalledVersion = app.app.additionalSettings['useVersionCodeAsOSVersion'] == true - ? app.installedInfo?.versionCode.toString() - : app.installedInfo?.versionName; + ? app.installedInfo?.versionCode.toString() + : app.installedInfo?.versionName; bool isHTMLWithNoVersionDetection = (source.runtimeType == HTML().runtimeType && - (app.app.additionalSettings['versionExtractionRegEx'] as String?) - ?.isNotEmpty != - true); + (app.app.additionalSettings['versionExtractionRegEx'] as String?) + ?.isNotEmpty != + true); bool isDirectAPKLink = source.runtimeType == DirectAPKLink().runtimeType; return app.app.additionalSettings['trackOnly'] != true && app.app.additionalSettings['releaseDateAsVersion'] != true && @@ -1259,7 +1424,9 @@ class AppsProvider with ChangeNotifier { realInstalledVersion != null && app.app.installedVersion != null && (reconcileVersionDifferences( - realInstalledVersion, app.app.installedVersion!) != + realInstalledVersion, + app.app.installedVersion!, + ) != null || naiveStandardVersionDetection); } @@ -1267,20 +1434,22 @@ class AppsProvider with ChangeNotifier { // Given an App and it's on-device info... // Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version App? getCorrectedInstallStatusAppIfPossible( - App app, PackageInfo? installedInfo) { + App app, + PackageInfo? installedInfo, + ) { var modded = false; var trackOnly = app.additionalSettings['trackOnly'] == true; var versionDetectionIsStandard = app.additionalSettings['versionDetection'] == true; var naiveStandardVersionDetection = app.additionalSettings['naiveStandardVersionDetection'] == true || - SourceProvider() - .getSource(app.url, overrideSource: app.overrideSource) - .naiveStandardVersionDetection; + SourceProvider() + .getSource(app.url, overrideSource: app.overrideSource) + .naiveStandardVersionDetection; String? realInstalledVersion = app.additionalSettings['useVersionCodeAsOSVersion'] == true - ? installedInfo?.versionCode.toString() - : installedInfo?.versionName; + ? installedInfo?.versionCode.toString() + : installedInfo?.versionName; // FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL if (installedInfo == null && app.installedVersion != null && !trackOnly) { // App says it's installed but isn't really (and isn't track only) - set to not installed @@ -1298,7 +1467,9 @@ class AppsProvider with ChangeNotifier { // App's reported version and real version don't match (and it uses standard version detection) // If they share a standard format (and are still different under it), update the reported version accordingly var correctedInstalledVersion = reconcileVersionDifferences( - realInstalledVersion, app.installedVersion!); + realInstalledVersion, + app.installedVersion!, + ); if (correctedInstalledVersion?.key == false) { app.installedVersion = correctedInstalledVersion!.value; modded = true; @@ -1313,8 +1484,10 @@ class AppsProvider with ChangeNotifier { versionDetectionIsStandard) { // App's reported installed and latest versions don't match (and it uses standard version detection) // If they share a standard format, make sure the App's reported installed version uses that format - var correctedInstalledVersion = - reconcileVersionDifferences(app.installedVersion!, app.latestVersion); + var correctedInstalledVersion = reconcileVersionDifferences( + app.installedVersion!, + app.latestVersion, + ); if (correctedInstalledVersion?.key == true) { app.installedVersion = correctedInstalledVersion!.value; modded = true; @@ -1324,7 +1497,8 @@ class AppsProvider with ChangeNotifier { if (installedInfo != null && versionDetectionIsStandard && !isVersionDetectionPossible( - AppInMemory(app, null, installedInfo, null))) { + AppInMemory(app, null, installedInfo, null), + )) { app.additionalSettings['versionDetection'] = false; app.installedVersion = app.latestVersion; logs.add('Could not reconcile version formats for: ${app.id}'); @@ -1335,27 +1509,39 @@ class AppsProvider with ChangeNotifier { } MapEntry? reconcileVersionDifferences( - String templateVersion, String comparisonVersion) { + String templateVersion, + String comparisonVersion, + ) { // Returns null if the versions don't share a common standard format // Returns if they share a common format and are equal // Returns if they share a common format but are not equal // templateVersion must fully match a standard format, while comparisonVersion can have a substring match - var templateVersionFormats = - findStandardFormatsForVersion(templateVersion, true); - var comparisonVersionFormats = - findStandardFormatsForVersion(comparisonVersion, true); + var templateVersionFormats = findStandardFormatsForVersion( + templateVersion, + true, + ); + var comparisonVersionFormats = findStandardFormatsForVersion( + comparisonVersion, + true, + ); if (comparisonVersionFormats.isEmpty) { - comparisonVersionFormats = - findStandardFormatsForVersion(comparisonVersion, false); + comparisonVersionFormats = findStandardFormatsForVersion( + comparisonVersion, + false, + ); } - var commonStandardFormats = - templateVersionFormats.intersection(comparisonVersionFormats); + var commonStandardFormats = templateVersionFormats.intersection( + comparisonVersionFormats, + ); if (commonStandardFormats.isEmpty) { return null; } for (String pattern in commonStandardFormats) { if (doStringsMatchUnderRegEx( - pattern, comparisonVersion, templateVersion)) { + pattern, + comparisonVersion, + templateVersion, + )) { return MapEntry(true, comparisonVersion); } } @@ -1368,7 +1554,7 @@ class AppsProvider with ChangeNotifier { var m2 = r.firstMatch(value2); return m1 != null && m2 != null ? value1.substring(m1.start, m1.end) == - value2.substring(m2.start, m2.end) + value2.substring(m2.start, m2.end) : false; } @@ -1382,70 +1568,90 @@ class AppsProvider with ChangeNotifier { List> errors = []; var installedAppsData = await getAllInstalledInfo(); List removedAppIds = []; - await Future.wait((await getAppsDir()) // Parse Apps from JSON - .listSync() - .map((item) async { - App? app; - if (item.path.toLowerCase().endsWith('.json') && - (singleId == null || - item.path.split('/').last.toLowerCase() == - '${singleId.toLowerCase()}.json')) { - try { - app = App.fromJson(jsonDecode(File(item.path).readAsStringSync())); - } catch (err) { - if (err is FormatException) { - logs.add('Corrupt JSON when loading App (will be ignored): $e'); - item.renameSync('${item.path}.corrupt'); - } else { - rethrow; - } - } - } - if (app != null) { - // Save the app to the in-memory list without grabbing any OS info first - apps.update( - app.id, - (value) => AppInMemory( - app!, value.downloadProgress, value.installedInfo, value.icon), - ifAbsent: () => AppInMemory(app!, null, null, null)); - notifyListeners(); - try { - // Try getting the app's source to ensure no invalid apps get loaded - sp.getSource(app.url, overrideSource: app.overrideSource); - // If the app is installed, grab its OS data and reconcile install statuses - PackageInfo? installedInfo; - try { - installedInfo = - installedAppsData.firstWhere((i) => i.packageName == app!.id); - } catch (e) { - // If the app isn't installed the above throws an error - } - // Reconcile differences between the installed and recorded install info - var moddedApp = - getCorrectedInstallStatusAppIfPossible(app, installedInfo); - if (moddedApp != null) { - app = moddedApp; - // Note the app ID if it was uninstalled externally - if (moddedApp.installedVersion == null) { - removedAppIds.add(moddedApp.id); + await Future.wait( + (await getAppsDir()) // Parse Apps from JSON + .listSync() + .map((item) async { + App? app; + if (item.path.toLowerCase().endsWith('.json') && + (singleId == null || + item.path.split('/').last.toLowerCase() == + '${singleId.toLowerCase()}.json')) { + try { + app = App.fromJson( + jsonDecode(File(item.path).readAsStringSync()), + ); + } catch (err) { + if (err is FormatException) { + logs.add( + 'Corrupt JSON when loading App (will be ignored): $e', + ); + item.renameSync('${item.path}.corrupt'); + } else { + rethrow; + } + } } - } - // Update the app in memory with install info and corrections - apps.update( - app.id, - (value) => AppInMemory( - app!, value.downloadProgress, installedInfo, value.icon), - ifAbsent: () => AppInMemory(app!, null, installedInfo, null)); - notifyListeners(); - } catch (e) { - errors.add([app!.id, app.finalName, e.toString()]); - } - } - })); + if (app != null) { + // Save the app to the in-memory list without grabbing any OS info first + apps.update( + app.id, + (value) => AppInMemory( + app!, + value.downloadProgress, + value.installedInfo, + value.icon, + ), + ifAbsent: () => AppInMemory(app!, null, null, null), + ); + notifyListeners(); + try { + // Try getting the app's source to ensure no invalid apps get loaded + sp.getSource(app.url, overrideSource: app.overrideSource); + // If the app is installed, grab its OS data and reconcile install statuses + PackageInfo? installedInfo; + try { + installedInfo = installedAppsData.firstWhere( + (i) => i.packageName == app!.id, + ); + } catch (e) { + // If the app isn't installed the above throws an error + } + // Reconcile differences between the installed and recorded install info + var moddedApp = getCorrectedInstallStatusAppIfPossible( + app, + installedInfo, + ); + if (moddedApp != null) { + app = moddedApp; + // Note the app ID if it was uninstalled externally + if (moddedApp.installedVersion == null) { + removedAppIds.add(moddedApp.id); + } + } + // Update the app in memory with install info and corrections + apps.update( + app.id, + (value) => AppInMemory( + app!, + value.downloadProgress, + installedInfo, + value.icon, + ), + ifAbsent: () => AppInMemory(app!, null, installedInfo, null), + ); + notifyListeners(); + } catch (e) { + errors.add([app!.id, app.finalName, e.toString()]); + } + } + }), + ); if (errors.isNotEmpty) { removeApps(errors.map((e) => e[0]).toList()); NotificationsProvider().notify( - AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); + AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()), + ); } // Delete externally uninstalled Apps if needed if (removedAppIds.isNotEmpty) { @@ -1471,66 +1677,86 @@ class AppsProvider with ChangeNotifier { } if (icon != null) { apps.update( - apps[appId]!.app.id, - (value) => AppInMemory(apps[appId]!.app, value.downloadProgress, - value.installedInfo, icon), - ifAbsent: () => AppInMemory( - apps[appId]!.app, null, apps[appId]?.installedInfo, icon)); + apps[appId]!.app.id, + (value) => AppInMemory( + apps[appId]!.app, + value.downloadProgress, + value.installedInfo, + icon, + ), + ifAbsent: () => AppInMemory( + apps[appId]!.app, + null, + apps[appId]?.installedInfo, + icon, + ), + ); notifyListeners(); } } } - Future saveApps(List apps, - {bool attemptToCorrectInstallStatus = true, - bool onlyIfExists = true}) async { + Future saveApps( + List apps, { + bool attemptToCorrectInstallStatus = true, + bool onlyIfExists = true, + }) async { attemptToCorrectInstallStatus = attemptToCorrectInstallStatus; - await Future.wait(apps.map((a) async { - var app = a.deepCopy(); - PackageInfo? info = await getInstalledInfo(app.id); - var icon = await info?.applicationInfo?.getAppIcon(); - app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name; - if (attemptToCorrectInstallStatus) { - app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; - } - if (!onlyIfExists || this.apps.containsKey(app.id)) { - 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, - (value) => AppInMemory(app, value.downloadProgress, info, icon), - ifAbsent: - onlyIfExists ? null : () => AppInMemory(app, null, info, icon)); - } catch (e) { - if (e is! ArgumentError || e.name != 'key') { - rethrow; + await Future.wait( + apps.map((a) async { + var app = a.deepCopy(); + PackageInfo? info = await getInstalledInfo(app.id); + var icon = await info?.applicationInfo?.getAppIcon(); + app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name; + if (attemptToCorrectInstallStatus) { + app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; } - } - })); + if (!onlyIfExists || this.apps.containsKey(app.id)) { + 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, + (value) => AppInMemory(app, value.downloadProgress, info, icon), + ifAbsent: onlyIfExists + ? null + : () => AppInMemory(app, null, info, icon), + ); + } catch (e) { + if (e is! ArgumentError || e.name != 'key') { + rethrow; + } + } + }), + ); notifyListeners(); export(isAuto: true); } Future removeApps(List appIds) async { var apkFiles = APKDir.listSync(); - await Future.wait(appIds.map((appId) async { - File file = File('${(await getAppsDir()).path}/$appId.json'); - if (file.existsSync()) { - deleteFile(file); - } - apkFiles - .where( - (element) => element.path.split('/').last.startsWith('$appId-')) - .forEach((element) { - element.delete(recursive: true); - }); - if (apps.containsKey(appId)) { - apps.remove(appId); - } - })); + await Future.wait( + appIds.map((appId) async { + File file = File('${(await getAppsDir()).path}/$appId.json'); + if (file.existsSync()) { + deleteFile(file); + } + apkFiles + .where( + (element) => element.path.split('/').last.startsWith('$appId-'), + ) + .forEach((element) { + element.delete(recursive: true); + }); + if (apps.containsKey(appId)) { + apps.remove(appId); + } + }), + ); if (appIds.isNotEmpty) { notifyListeners(); export(isAuto: true); @@ -1539,31 +1765,39 @@ class AppsProvider with ChangeNotifier { Future removeAppsWithModal(BuildContext context, List apps) async { var showUninstallOption = apps - .where((a) => - a.installedVersion != null && - a.additionalSettings['trackOnly'] != true) + .where( + (a) => + a.installedVersion != null && + a.additionalSettings['trackOnly'] != true, + ) .isNotEmpty; var values = await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - primaryActionColour: Theme.of(context).colorScheme.error, - title: plural('removeAppQuestion', apps.length), - items: !showUninstallOption - ? [] - : [ - [ - GeneratedFormSwitch('rmAppEntry', - label: tr('removeFromObtainium'), defaultValue: true) - ], - [ - GeneratedFormSwitch('uninstallApp', - label: tr('uninstallFromDevice')) - ] + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + primaryActionColour: Theme.of(context).colorScheme.error, + title: plural('removeAppQuestion', apps.length), + items: !showUninstallOption + ? [] + : [ + [ + GeneratedFormSwitch( + 'rmAppEntry', + label: tr('removeFromObtainium'), + defaultValue: true, + ), ], - initValid: true, - ); - }); + [ + GeneratedFormSwitch( + 'uninstallApp', + label: tr('uninstallFromDevice'), + ), + ], + ], + initValid: true, + ); + }, + ); if (values != null) { bool uninstall = values['uninstallApp'] == true && showUninstallOption; bool remove = values['rmAppEntry'] == true || !showUninstallOption; @@ -1608,11 +1842,14 @@ class AppsProvider with ChangeNotifier { App? currentApp = apps[appId]!.app; SourceProvider sourceProvider = SourceProvider(); App newApp = await sourceProvider.getApp( - sourceProvider.getSource(currentApp.url, - overrideSource: currentApp.overrideSource), + sourceProvider.getSource( currentApp.url, - currentApp.additionalSettings, - currentApp: currentApp); + overrideSource: currentApp.overrideSource, + ), + currentApp.url, + currentApp.additionalSettings, + currentApp: currentApp, + ); if (currentApp.preferredApkIndex < newApp.apkUrls.length) { newApp.preferredApkIndex = currentApp.preferredApkIndex; } @@ -1620,14 +1857,17 @@ class AppsProvider with ChangeNotifier { return newApp.latestVersion != currentApp.latestVersion ? newApp : null; } - List getAppsSortedByUpdateCheckTime( - {DateTime? ignoreAppsCheckedAfter, - bool onlyCheckInstalledOrTrackOnlyApps = false}) { + List getAppsSortedByUpdateCheckTime({ + DateTime? ignoreAppsCheckedAfter, + bool onlyCheckInstalledOrTrackOnlyApps = false, + }) { List appIds = apps.values - .where((app) => - app.app.lastUpdateCheck == null || - ignoreAppsCheckedAfter == null || - app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter)) + .where( + (app) => + app.app.lastUpdateCheck == null || + ignoreAppsCheckedAfter == null || + app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter), + ) .where((app) { if (!onlyCheckInstalledOrTrackOnlyApps) { return true; @@ -1638,18 +1878,24 @@ class AppsProvider with ChangeNotifier { }) .map((e) => e.app.id) .toList(); - appIds.sort((a, b) => - (apps[a]!.app.lastUpdateCheck ?? DateTime.fromMicrosecondsSinceEpoch(0)) - .compareTo(apps[b]!.app.lastUpdateCheck ?? - DateTime.fromMicrosecondsSinceEpoch(0))); + appIds.sort( + (a, b) => + (apps[a]!.app.lastUpdateCheck ?? + DateTime.fromMicrosecondsSinceEpoch(0)) + .compareTo( + apps[b]!.app.lastUpdateCheck ?? + DateTime.fromMicrosecondsSinceEpoch(0), + ), + ); return appIds; } - Future> checkUpdates( - {DateTime? ignoreAppsCheckedAfter, - bool throwErrorsForRetry = false, - List? specificIds, - SettingsProvider? sp}) async { + Future> checkUpdates({ + DateTime? ignoreAppsCheckedAfter, + bool throwErrorsForRetry = false, + List? specificIds, + SettingsProvider? sp, + }) async { SettingsProvider settingsProvider = sp ?? this.settingsProvider; List updates = []; MultiAppMultiError errors = MultiAppMultiError(); @@ -1657,27 +1903,31 @@ class AppsProvider with ChangeNotifier { gettingUpdates = true; try { List appIds = getAppsSortedByUpdateCheckTime( - ignoreAppsCheckedAfter: ignoreAppsCheckedAfter, - onlyCheckInstalledOrTrackOnlyApps: - settingsProvider.onlyCheckInstalledOrTrackOnlyApps); + ignoreAppsCheckedAfter: ignoreAppsCheckedAfter, + onlyCheckInstalledOrTrackOnlyApps: + settingsProvider.onlyCheckInstalledOrTrackOnlyApps, + ); if (specificIds != null) { appIds = appIds.where((aId) => specificIds.contains(aId)).toList(); } - await Future.wait(appIds.map((appId) async { - App? newApp; - try { - newApp = await checkUpdate(appId); - } catch (e) { - if ((e is RateLimitError || e is SocketException) && - throwErrorsForRetry) { - rethrow; + await Future.wait( + appIds.map((appId) async { + App? newApp; + try { + newApp = await checkUpdate(appId); + } catch (e) { + if ((e is RateLimitError || e is SocketException) && + throwErrorsForRetry) { + rethrow; + } + errors.add(appId, e, appName: apps[appId]?.name); } - errors.add(appId, e, appName: apps[appId]?.name); - } - if (newApp != null) { - updates.add(newApp); - } - }), eagerError: true); + if (newApp != null) { + updates.add(newApp); + } + }), + eagerError: true, + ); } finally { gettingUpdates = false; } @@ -1691,8 +1941,10 @@ class AppsProvider with ChangeNotifier { return updates; } - List findExistingUpdates( - {bool installedOnly = false, bool nonInstalledOnly = false}) { + List findExistingUpdates({ + bool installedOnly = false, + bool nonInstalledOnly = false, + }) { List updateAppIds = []; List appIds = apps.keys.toList(); for (int i = 0; i < appIds.length; i++) { @@ -1710,8 +1962,10 @@ class AppsProvider with ChangeNotifier { return updateAppIds; } - Map generateExportJSON( - {List? appIds, bool? overrideExportSettings}) { + Map generateExportJSON({ + List? appIds, + bool? overrideExportSettings, + }) { Map finalExport = {}; finalExport['apps'] = apps.values .where((e) { @@ -1729,17 +1983,21 @@ class AppsProvider with ChangeNotifier { } if (shouldExportSettings) { finalExport['settings'] = Map.fromEntries( - (settingsProvider.prefs - ?.getKeys() - .map((key) => MapEntry(key, settingsProvider.prefs?.get(key))) - .toList()) ?? - []); + (settingsProvider.prefs + ?.getKeys() + .map((key) => MapEntry(key, settingsProvider.prefs?.get(key))) + .toList()) ?? + [], + ); } return finalExport; } - Future export( - {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async { + Future export({ + bool pickOnly = false, + isAuto = false, + SettingsProvider? sp, + }) async { SettingsProvider settingsProvider = sp ?? this.settingsProvider; var exportDir = await settingsProvider.getExportDir(); if (isAuto) { @@ -1770,16 +2028,19 @@ class AppsProvider with ChangeNotifier { if (!pickOnly) { var encoder = const JsonEncoder.withIndent(" "); Map finalExport = generateExportJSON(); - var result = await saf.createFile(exportDir, - displayName: - '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', - mimeType: 'application/json', - bytes: Uint8List.fromList(utf8.encode(encoder.convert(finalExport)))); + var result = await saf.createFile( + exportDir, + displayName: + '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', + mimeType: 'application/json', + bytes: Uint8List.fromList(utf8.encode(encoder.convert(finalExport))), + ); if (result == null) { throw ObtainiumError(tr('unexpectedError')); } - returnPath = - exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/'); + returnPath = exportDir.pathSegments + .join('/') + .replaceFirst('tree/primary:', '/'); } return returnPath; } @@ -1798,8 +2059,8 @@ class AppsProvider with ChangeNotifier { var installedInfo = await getInstalledInfo(a.id, printErr: false); a.installedVersion = a.additionalSettings['useVersionCodeAsOSVersion'] == true - ? installedInfo?.versionCode.toString() - : installedInfo?.versionName; + ? installedInfo?.versionCode.toString() + : installedInfo?.versionName; } await saveApps(importedApps, onlyIfExists: false); notifyListeners(); @@ -1813,15 +2074,19 @@ class AppsProvider with ChangeNotifier { } else if (value is bool) { settingsProvider.prefs?.setBool(key, value); } else if (value is List) { - settingsProvider.prefs - ?.setStringList(key, value.map((e) => e as String).toList()); + settingsProvider.prefs?.setStringList( + key, + value.map((e) => e as String).toList(), + ); } else { settingsProvider.prefs?.setString(key, value as String); } }); } return MapEntry, bool>( - importedApps, newFormat && decodedJSON['settings'] != null); + importedApps, + newFormat && decodedJSON['settings'] != null, + ); } @override @@ -1830,11 +2095,15 @@ class AppsProvider with ChangeNotifier { super.dispose(); } - Future>> addAppsByURL(List urls, - {AppSource? sourceOverride}) async { - List results = await SourceProvider().getAppsByURLNaive(urls, - alreadyAddedUrls: apps.values.map((e) => e.app.url).toList(), - sourceOverride: sourceOverride); + Future>> addAppsByURL( + List urls, { + AppSource? sourceOverride, + }) async { + List results = await SourceProvider().getAppsByURLNaive( + urls, + alreadyAddedUrls: apps.values.map((e) => e.app.url).toList(), + sourceOverride: sourceOverride, + ); List pps = results[0]; Map errorsMap = results[1]; for (var app in pps) { @@ -1844,19 +2113,21 @@ class AppsProvider with ChangeNotifier { await saveApps([app], onlyIfExists: false); } } - List> errors = - errorsMap.keys.map((e) => [e, errorsMap[e].toString()]).toList(); + List> errors = errorsMap.keys + .map((e) => [e, errorsMap[e].toString()]) + .toList(); return errors; } } class AppFilePicker extends StatefulWidget { - const AppFilePicker( - {super.key, - required this.app, - this.initVal, - this.archs, - this.pickAnyAsset = false}); + const AppFilePicker({ + super.key, + required this.app, + this.initVal, + this.archs, + this.pickAnyAsset = false, + }); final App app; final MapEntry? initVal; @@ -1879,16 +2150,21 @@ class _AppFilePickerState extends State { } return AlertDialog( scrollable: true, - title: Text(widget.pickAnyAsset - ? tr('selectX', args: [tr('releaseAsset').toLowerCase()]) - : tr('pickAnAPK')), - content: Column(children: [ - urlsToSelectFrom.length > 1 - ? Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])) - : const SizedBox.shrink(), - const SizedBox(height: 16), - ...urlsToSelectFrom.map( - (u) => RadioListTile( + title: Text( + widget.pickAnyAsset + ? tr('selectX', args: [tr('releaseAsset').toLowerCase()]) + : tr('pickAnAPK'), + ), + content: Column( + children: [ + urlsToSelectFrom.length > 1 + ? Text( + tr('appHasMoreThanOnePackage', args: [widget.app.finalName]), + ) + : const SizedBox.shrink(), + const SizedBox(height: 16), + ...urlsToSelectFrom.map( + (u) => RadioListTile( title: Text(u.key), value: u.value, groupValue: fileUrl!.value, @@ -1896,42 +2172,47 @@ class _AppFilePickerState extends State { setState(() { fileUrl = urlsToSelectFrom.where((e) => e.value == val).first; }); - }), - ), - if (widget.archs != null) - const SizedBox( - height: 16, + }, + ), ), - if (widget.archs != null) - Text( - widget.archs!.length == 1 - ? tr('deviceSupportsXArch', args: [widget.archs![0]]) - : tr('deviceSupportsFollowingArchs') + - list2FriendlyString( - widget.archs!.map((e) => '\'$e\'').toList()), - style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), - ), - ]), + if (widget.archs != null) const SizedBox(height: 16), + if (widget.archs != null) + Text( + widget.archs!.length == 1 + ? tr('deviceSupportsXArch', args: [widget.archs![0]]) + : tr('deviceSupportsFollowingArchs') + + list2FriendlyString( + widget.archs!.map((e) => '\'$e\'').toList(), + ), + style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), + ), + ], + ), actions: [ TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(tr('cancel'))), + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(tr('cancel')), + ), TextButton( - onPressed: () { - HapticFeedback.selectionClick(); - Navigator.of(context).pop(fileUrl); - }, - child: Text(tr('continue'))) + onPressed: () { + HapticFeedback.selectionClick(); + Navigator.of(context).pop(fileUrl); + }, + child: Text(tr('continue')), + ), ], ); } } class APKOriginWarningDialog extends StatefulWidget { - const APKOriginWarningDialog( - {super.key, required this.sourceUrl, required this.apkUrl}); + const APKOriginWarningDialog({ + super.key, + required this.sourceUrl, + required this.apkUrl, + }); final String sourceUrl; final String apkUrl; @@ -1946,22 +2227,29 @@ class _APKOriginWarningDialogState extends State { return AlertDialog( scrollable: true, title: Text(tr('warning')), - content: Text(tr('sourceIsXButPackageFromYPrompt', args: [ - Uri.parse(widget.sourceUrl).host, - Uri.parse(widget.apkUrl).host - ])), + content: Text( + tr( + 'sourceIsXButPackageFromYPrompt', + args: [ + Uri.parse(widget.sourceUrl).host, + Uri.parse(widget.apkUrl).host, + ], + ), + ), actions: [ TextButton( - onPressed: () { - Navigator.of(context).pop(null); - }, - child: Text(tr('cancel'))), + onPressed: () { + Navigator.of(context).pop(null); + }, + child: Text(tr('cancel')), + ), TextButton( - onPressed: () { - HapticFeedback.selectionClick(); - Navigator.of(context).pop(true); - }, - child: Text(tr('continue'))) + onPressed: () { + HapticFeedback.selectionClick(); + Navigator.of(context).pop(true); + }, + child: Text(tr('continue')), + ), ], ); } @@ -2007,41 +2295,54 @@ Future bgUpdateCheck(String taskId, Map? params) async { params ??= {}; - bool firstEverUpdateTask = DateTime.fromMillisecondsSinceEpoch(0) - .compareTo(appsProvider.settingsProvider.lastCompletedBGCheckTime) == + bool firstEverUpdateTask = + DateTime.fromMillisecondsSinceEpoch( + 0, + ).compareTo(appsProvider.settingsProvider.lastCompletedBGCheckTime) == 0; List> toCheck = >[ ...(params['toCheck'] - ?.map((entry) => MapEntry( - entry['key'] as String, entry['value'] as int)) + ?.map( + (entry) => MapEntry( + entry['key'] as String, + entry['value'] as int, + ), + ) .toList() ?? appsProvider .getAppsSortedByUpdateCheckTime( - ignoreAppsCheckedAfter: params['toCheck'] == null - ? firstEverUpdateTask + ignoreAppsCheckedAfter: params['toCheck'] == null + ? firstEverUpdateTask ? null : appsProvider.settingsProvider.lastCompletedBGCheckTime - : null, - onlyCheckInstalledOrTrackOnlyApps: appsProvider - .settingsProvider.onlyCheckInstalledOrTrackOnlyApps) - .map((e) => MapEntry(e, 0))) + : null, + onlyCheckInstalledOrTrackOnlyApps: appsProvider + .settingsProvider + .onlyCheckInstalledOrTrackOnlyApps, + ) + .map((e) => MapEntry(e, 0))), ]; List> toInstall = >[ ...(params['toInstall'] - ?.map((entry) => MapEntry( - entry['key'] as String, entry['value'] as int)) + ?.map( + (entry) => MapEntry( + entry['key'] as String, + entry['value'] as int, + ), + ) .toList() ?? - (>>[])) + (>>[])), ]; - var networkRestricted = appsProvider.settingsProvider.bgUpdatesOnWiFiOnly && + var networkRestricted = + appsProvider.settingsProvider.bgUpdatesOnWiFiOnly && !netResult.contains(ConnectivityResult.wifi) && !netResult.contains(ConnectivityResult.ethernet); var chargingRestricted = appsProvider.settingsProvider.bgUpdatesWhileChargingOnly && - (await Battery().batteryState) != BatteryState.charging; + (await Battery().batteryState) != BatteryState.charging; if (networkRestricted) { logs.add('BG update task: Network restriction in effect.'); @@ -2060,15 +2361,18 @@ Future bgUpdateCheck(String taskId, Map? params) async { // After grouping the updates, we take care of toNotify and toThrow first // Then we run the function again in install mode (toCheck is empty) - var enoughTimePassed = appsProvider.settingsProvider.updateInterval != 0 && + var enoughTimePassed = + appsProvider.settingsProvider.updateInterval != 0 && appsProvider.settingsProvider.lastCompletedBGCheckTime .add( - Duration(minutes: appsProvider.settingsProvider.updateInterval)) + Duration(minutes: appsProvider.settingsProvider.updateInterval), + ) .isBefore(DateTime.now()); if (!enoughTimePassed) { // ignore: avoid_print print( - 'BG update task: Too early for another check (last check was ${appsProvider.settingsProvider.lastCompletedBGCheckTime.toIso8601String()}, interval is ${appsProvider.settingsProvider.updateInterval}).'); + 'BG update task: Too early for another check (last check was ${appsProvider.settingsProvider.lastCompletedBGCheckTime.toIso8601String()}, interval is ${appsProvider.settingsProvider.updateInterval}).', + ); return; } @@ -2082,25 +2386,28 @@ Future bgUpdateCheck(String taskId, Map? params) async { []; // All apps that got errors while checking var retryAfterXSeconds = 0; MultiAppMultiError? - errors; // All errors including those that will lead to a retry + errors; // All errors including those that will lead to a retry MultiAppMultiError toThrow = MultiAppMultiError(); // All errors that will not lead to a retry, just a notification CheckingUpdatesNotification notif = CheckingUpdatesNotification( - plural('apps', toCheck.length)); // The notif. to show while checking + plural('apps', toCheck.length), + ); // The notif. to show while checking try { // Check for updates notificationsProvider.notify(notif, cancelExisting: true); updates = await appsProvider.checkUpdates( - specificIds: toCheck.map((e) => e.key).toList(), - sp: appsProvider.settingsProvider); + specificIds: toCheck.map((e) => e.key).toList(), + sp: appsProvider.settingsProvider, + ); } catch (e) { if (e is Map) { updates = e['updates']; errors = e['errors']; errors!.rawErrors.forEach((key, err) { logs.add( - 'BG update task: Got error on checking for $key \'${err.toString()}\'.'); + 'BG update task: Got error on checking for $key \'${err.toString()}\'.', + ); var toCheckApp = toCheck.where((element) => element.key == key).first; if (toCheckApp.value < maxAttempts) { @@ -2109,8 +2416,8 @@ Future bgUpdateCheck(String taskId, Map? params) async { int minRetryIntervalForThisApp = err is RateLimitError ? (err.remainingMinutes * 60) : e is ClientException - ? (15 * 60) - : (toCheckApp.value + 1); + ? (15 * 60) + : (toCheckApp.value + 1); if (minRetryIntervalForThisApp > maxRetryWaitSeconds) { minRetryIntervalForThisApp = maxRetryWaitSeconds; } @@ -2151,16 +2458,20 @@ Future bgUpdateCheck(String taskId, Map? params) async { // Send the error notifications (grouped by error string) if (toThrow.rawErrors.isNotEmpty) { for (var element in toThrow.idsByErrorString.entries) { - notificationsProvider.notify(ErrorCheckingUpdatesNotification( + notificationsProvider.notify( + ErrorCheckingUpdatesNotification( errors!.errorsAppsString(element.key, element.value), - id: Random().nextInt(10000))); + id: Random().nextInt(10000), + ), + ); } } // if there are update checks to retry, schedule a retry task logs.add('BG update task: Done checking for updates.'); if (toRetry.isNotEmpty) { logs.add( - 'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.'); + 'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.', + ); return await bgUpdateCheck(taskId, { 'toCheck': toRetry .map((entry) => {'key': entry.key, 'value': entry.value}) @@ -2176,7 +2487,7 @@ Future bgUpdateCheck(String taskId, Map? params) async { 'toCheck': [], 'toInstall': toInstall .map((entry) => {'key': entry.key, 'value': entry.value}) - .toList() + .toList(), }); } } else { @@ -2185,8 +2496,9 @@ Future bgUpdateCheck(String taskId, Map? params) async { if (toInstall.isEmpty && !networkRestricted && !chargingRestricted) { var temp = appsProvider.findExistingUpdates(installedOnly: true); for (var i = 0; i < temp.length; i++) { - if (await appsProvider - .canInstallSilently(appsProvider.apps[temp[i]]!.app)) { + if (await appsProvider.canInstallSilently( + appsProvider.apps[temp[i]]!.app, + )) { toInstall.add(MapEntry(temp[i], 0)); } } @@ -2202,14 +2514,17 @@ Future bgUpdateCheck(String taskId, Map? params) async { // Loop through all updates and install each try { await appsProvider.downloadAndInstallLatestApps( - toInstall.map((e) => e.key).toList(), null, - notificationsProvider: notificationsProvider, - forceParallelDownloads: true); + toInstall.map((e) => e.key).toList(), + null, + notificationsProvider: notificationsProvider, + forceParallelDownloads: true, + ); } catch (e) { if (e is MultiAppMultiError) { e.idsByErrorString.forEach((key, value) { - notificationsProvider.notify(ErrorCheckingUpdatesNotification( - e.errorsAppsString(key, value))); + notificationsProvider.notify( + ErrorCheckingUpdatesNotification(e.errorsAppsString(key, value)), + ); }); } else { // We don't expect to ever get here in any situation so no need to catch (but log it in case) diff --git a/lib/providers/logs_provider.dart b/lib/providers/logs_provider.dart index 8dab366..a719bd4 100644 --- a/lib/providers/logs_provider.dart +++ b/lib/providers/logs_provider.dart @@ -22,7 +22,7 @@ class Log { idColumn: id, levelColumn: level.index, messageColumn: message, - timestampColumn: timestamp.millisecondsSinceEpoch + timestampColumn: timestamp.millisecondsSinceEpoch, }; return map; } @@ -33,8 +33,9 @@ class Log { id = map[idColumn] as int; level = LogLevels.values.elementAt(map[levelColumn] as int); message = map[messageColumn] as String; - timestamp = - DateTime.fromMillisecondsSinceEpoch(map[timestampColumn] as int); + timestamp = DateTime.fromMillisecondsSinceEpoch( + map[timestampColumn] as int, + ); } @override @@ -51,16 +52,19 @@ class LogsProvider { Database? db; Future getDB() async { - db ??= await openDatabase(dbPath, version: 1, - onCreate: (Database db, int version) async { - await db.execute(''' + db ??= await openDatabase( + dbPath, + version: 1, + onCreate: (Database db, int version) async { + await db.execute(''' create table if not exists $logTable ( $idColumn integer primary key autoincrement, $levelColumn integer not null, $messageColumn text not null, $timestampColumn integer not null) '''); - }); + }, + ); return db!; } @@ -75,27 +79,38 @@ create table if not exists $logTable ( Future> get({DateTime? before, DateTime? after}) async { var where = getWhereDates(before: before, after: after); - return (await (await getDB()) - .query(logTable, where: where.key, whereArgs: where.value)) - .map((e) => Log.fromMap(e)) - .toList(); + return (await (await getDB()).query( + logTable, + where: where.key, + whereArgs: where.value, + )).map((e) => Log.fromMap(e)).toList(); } Future clear({DateTime? before, DateTime? after}) async { var where = getWhereDates(before: before, after: after); - var res = await (await getDB()) - .delete(logTable, where: where.key, whereArgs: where.value); + var res = await (await getDB()).delete( + logTable, + where: where.key, + whereArgs: where.value, + ); if (res > 0) { - add(plural('clearedNLogsBeforeXAfterY', res, + add( + plural( + 'clearedNLogsBeforeXAfterY', + res, namedArgs: {'before': before.toString(), 'after': after.toString()}, - name: 'n')); + name: 'n', + ), + ); } return res; } } -MapEntry?> getWhereDates( - {DateTime? before, DateTime? after}) { +MapEntry?> getWhereDates({ + DateTime? before, + DateTime? after, +}) { List where = []; List whereArgs = []; if (before != null) { diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index bc329cf..0f126ce 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -20,91 +20,116 @@ class ObtainiumNotification { bool onlyAlertOnce; String? payload; - ObtainiumNotification(this.id, this.title, this.message, this.channelCode, - this.channelName, this.channelDescription, this.importance, - {this.onlyAlertOnce = false, this.progPercent, this.payload}); + ObtainiumNotification( + this.id, + this.title, + this.message, + this.channelCode, + this.channelName, + this.channelDescription, + this.importance, { + this.onlyAlertOnce = false, + this.progPercent, + this.payload, + }); } class UpdateNotification extends ObtainiumNotification { UpdateNotification(List updates, {int? id}) - : super( - id ?? 2, - tr('updatesAvailable'), - '', - 'UPDATES_AVAILABLE', - tr('updatesAvailableNotifChannel'), - tr('updatesAvailableNotifDescription'), - Importance.max) { + : super( + id ?? 2, + tr('updatesAvailable'), + '', + 'UPDATES_AVAILABLE', + tr('updatesAvailableNotifChannel'), + tr('updatesAvailableNotifDescription'), + Importance.max, + ) { message = updates.isEmpty ? tr('noNewUpdates') : updates.length == 1 - ? tr('xHasAnUpdate', args: [updates[0].finalName]) - : plural('xAndNMoreUpdatesAvailable', updates.length - 1, - args: [updates[0].finalName, (updates.length - 1).toString()]); + ? tr('xHasAnUpdate', args: [updates[0].finalName]) + : plural( + 'xAndNMoreUpdatesAvailable', + updates.length - 1, + args: [updates[0].finalName, (updates.length - 1).toString()], + ); } } class SilentUpdateNotification extends ObtainiumNotification { SilentUpdateNotification(List updates, bool succeeded, {int? id}) - : super( - id ?? 3, - succeeded ? tr('appsUpdated') : tr('appsNotUpdated'), - '', - 'APPS_UPDATED', - tr('appsUpdatedNotifChannel'), - tr('appsUpdatedNotifDescription'), - Importance.defaultImportance) { + : super( + id ?? 3, + succeeded ? tr('appsUpdated') : tr('appsNotUpdated'), + '', + 'APPS_UPDATED', + tr('appsUpdatedNotifChannel'), + tr('appsUpdatedNotifDescription'), + Importance.defaultImportance, + ) { message = updates.length == 1 - ? tr(succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY', - args: [updates[0].finalName, updates[0].latestVersion]) + ? tr( + succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY', + args: [updates[0].finalName, updates[0].latestVersion], + ) : plural( succeeded ? 'xAndNMoreUpdatesInstalled' : "xAndNMoreUpdatesFailed", updates.length - 1, - args: [updates[0].finalName, (updates.length - 1).toString()]); + args: [updates[0].finalName, (updates.length - 1).toString()], + ); } } class SilentUpdateAttemptNotification extends ObtainiumNotification { SilentUpdateAttemptNotification(List updates, {int? id}) - : super( - id ?? 3, - tr('appsPossiblyUpdated'), - '', - 'APPS_POSSIBLY_UPDATED', - tr('appsPossiblyUpdatedNotifChannel'), - tr('appsPossiblyUpdatedNotifDescription'), - Importance.defaultImportance) { + : super( + id ?? 3, + tr('appsPossiblyUpdated'), + '', + 'APPS_POSSIBLY_UPDATED', + tr('appsPossiblyUpdatedNotifChannel'), + tr('appsPossiblyUpdatedNotifDescription'), + Importance.defaultImportance, + ) { message = updates.length == 1 - ? tr('xWasPossiblyUpdatedToY', - args: [updates[0].finalName, updates[0].latestVersion]) - : plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1, - args: [updates[0].finalName, (updates.length - 1).toString()]); + ? tr( + 'xWasPossiblyUpdatedToY', + args: [updates[0].finalName, updates[0].latestVersion], + ) + : plural( + 'xAndNMoreUpdatesPossiblyInstalled', + updates.length - 1, + args: [updates[0].finalName, (updates.length - 1).toString()], + ); } } class ErrorCheckingUpdatesNotification extends ObtainiumNotification { ErrorCheckingUpdatesNotification(String error, {int? id}) - : super( - id ?? 5, - tr('errorCheckingUpdates'), - error, - 'BG_UPDATE_CHECK_ERROR', - tr('errorCheckingUpdatesNotifChannel'), - tr('errorCheckingUpdatesNotifDescription'), - Importance.high, - payload: "${tr('errorCheckingUpdates')}\n$error"); + : super( + id ?? 5, + tr('errorCheckingUpdates'), + error, + 'BG_UPDATE_CHECK_ERROR', + tr('errorCheckingUpdatesNotifChannel'), + tr('errorCheckingUpdatesNotifDescription'), + Importance.high, + payload: "${tr('errorCheckingUpdates')}\n$error", + ); } class AppsRemovedNotification extends ObtainiumNotification { AppsRemovedNotification(List> namedReasons) - : super( - 6, - tr('appsRemoved'), - '', - 'APPS_REMOVED', - tr('appsRemovedNotifChannel'), - tr('appsRemovedNotifDescription'), - Importance.max) { + : super( + 6, + tr('appsRemoved'), + '', + 'APPS_REMOVED', + tr('appsRemovedNotifChannel'), + tr('appsRemovedNotifDescription'), + Importance.max, + ) { message = ''; for (var r in namedReasons) { message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n'; @@ -115,49 +140,53 @@ class AppsRemovedNotification extends ObtainiumNotification { class DownloadNotification extends ObtainiumNotification { DownloadNotification(String appName, int progPercent) - : super( - appName.hashCode, - tr('downloadingX', args: [appName]), - '', - 'APP_DOWNLOADING', - tr('downloadingXNotifChannel', args: [tr('app')]), - tr('downloadNotifDescription'), - Importance.low, - onlyAlertOnce: true, - progPercent: progPercent); + : super( + appName.hashCode, + tr('downloadingX', args: [appName]), + '', + 'APP_DOWNLOADING', + tr('downloadingXNotifChannel', args: [tr('app')]), + tr('downloadNotifDescription'), + Importance.low, + onlyAlertOnce: true, + progPercent: progPercent, + ); } class DownloadedNotification extends ObtainiumNotification { DownloadedNotification(String fileName, String downloadUrl) - : super( - downloadUrl.hashCode, - tr('downloadedX', args: [fileName]), - '', - 'FILE_DOWNLOADED', - tr('downloadedXNotifChannel', args: [tr('app')]), - tr('downloadedX', args: [tr('app')]), - Importance.defaultImportance); + : super( + downloadUrl.hashCode, + tr('downloadedX', args: [fileName]), + '', + 'FILE_DOWNLOADED', + tr('downloadedXNotifChannel', args: [tr('app')]), + tr('downloadedX', args: [tr('app')]), + Importance.defaultImportance, + ); } final completeInstallationNotification = ObtainiumNotification( - 1, - tr('completeAppInstallation'), - tr('obtainiumMustBeOpenToInstallApps'), - 'COMPLETE_INSTALL', - tr('completeAppInstallationNotifChannel'), - tr('completeAppInstallationNotifDescription'), - Importance.max); + 1, + tr('completeAppInstallation'), + tr('obtainiumMustBeOpenToInstallApps'), + 'COMPLETE_INSTALL', + tr('completeAppInstallationNotifChannel'), + tr('completeAppInstallationNotifDescription'), + Importance.max, +); class CheckingUpdatesNotification extends ObtainiumNotification { CheckingUpdatesNotification(String appName) - : super( - 4, - tr('checkingForUpdates'), - appName, - 'BG_UPDATE_CHECK', - tr('checkingForUpdatesNotifChannel'), - tr('checkingForUpdatesNotifDescription'), - Importance.min); + : super( + 4, + tr('checkingForUpdates'), + appName, + 'BG_UPDATE_CHECK', + tr('checkingForUpdatesNotifChannel'), + tr('checkingForUpdatesNotifDescription'), + Importance.min, + ); } class NotificationsProvider { @@ -173,13 +202,15 @@ class NotificationsProvider { Importance.max: Priority.max, Importance.min: Priority.min, Importance.none: Priority.min, - Importance.unspecified: Priority.defaultPriority + Importance.unspecified: Priority.defaultPriority, }; Future initialize() async { - isInitialized = await notifications.initialize( + isInitialized = + await notifications.initialize( const InitializationSettings( - android: AndroidInitializationSettings('ic_notification')), + android: AndroidInitializationSettings('ic_notification'), + ), onDidReceiveNotificationResponse: (NotificationResponse response) { _showNotificationPayload(response.payload); }, @@ -188,11 +219,13 @@ class NotificationsProvider { } checkLaunchByNotif() async { - final NotificationAppLaunchDetails? launchDetails = - await notifications.getNotificationAppLaunchDetails(); + final NotificationAppLaunchDetails? launchDetails = await notifications + .getNotificationAppLaunchDetails(); if (launchDetails?.didNotificationLaunchApp ?? false) { - _showNotificationPayload(launchDetails!.notificationResponse?.payload, - doublePop: true); + _showNotificationPayload( + launchDetails!.notificationResponse?.payload, + doublePop: true, + ); } } @@ -207,13 +240,14 @@ class NotificationsProvider { content: Text(content), actions: [ TextButton( - onPressed: () { + onPressed: () { + Navigator.of(context).pop(null); + if (doublePop) { Navigator.of(context).pop(null); - if (doublePop) { - Navigator.of(context).pop(null); - } - }, - child: Text(tr('ok'))), + } + }, + child: Text(tr('ok')), + ), ], ), ), @@ -229,17 +263,18 @@ class NotificationsProvider { } Future notifyRaw( - int id, - String title, - String message, - String channelCode, - String channelName, - String channelDescription, - Importance importance, - {bool cancelExisting = false, - int? progPercent, - bool onlyAlertOnce = false, - String? payload}) async { + int id, + String title, + String message, + String channelCode, + String channelName, + String channelDescription, + Importance importance, { + bool cancelExisting = false, + int? progPercent, + bool onlyAlertOnce = false, + String? payload, + }) async { if (cancelExisting) { await cancel(id); } @@ -247,29 +282,42 @@ class NotificationsProvider { await initialize(); } await notifications.show( - id, - title, - message, - NotificationDetails( - android: AndroidNotificationDetails(channelCode, channelName, - channelDescription: channelDescription, - importance: importance, - priority: importanceToPriority[importance]!, - groupKey: '$obtainiumId.$channelCode', - progress: progPercent ?? 0, - maxProgress: 100, - showProgress: progPercent != null, - onlyAlertOnce: onlyAlertOnce, - indeterminate: progPercent != null && progPercent < 0)), - payload: payload); + id, + title, + message, + NotificationDetails( + android: AndroidNotificationDetails( + channelCode, + channelName, + channelDescription: channelDescription, + importance: importance, + priority: importanceToPriority[importance]!, + groupKey: '$obtainiumId.$channelCode', + progress: progPercent ?? 0, + maxProgress: 100, + showProgress: progPercent != null, + onlyAlertOnce: onlyAlertOnce, + indeterminate: progPercent != null && progPercent < 0, + ), + ), + payload: payload, + ); } - Future notify(ObtainiumNotification notif, - {bool cancelExisting = false}) => - notifyRaw(notif.id, notif.title, notif.message, notif.channelCode, - notif.channelName, notif.channelDescription, notif.importance, - cancelExisting: cancelExisting, - onlyAlertOnce: notif.onlyAlertOnce, - progPercent: notif.progPercent, - payload: notif.payload); + Future notify( + ObtainiumNotification notif, { + bool cancelExisting = false, + }) => notifyRaw( + notif.id, + notif.title, + notif.message, + notif.channelCode, + notif.channelName, + notif.channelDescription, + notif.importance, + cancelExisting: cancelExisting, + onlyAlertOnce: notif.onlyAlertOnce, + progPercent: notif.progPercent, + payload: notif.payload, + ); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index e49e8e3..7870908 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -58,8 +58,8 @@ class SettingsProvider with ChangeNotifier { } ThemeSettings get theme { - return ThemeSettings - .values[prefs?.getInt('theme') ?? ThemeSettings.system.index]; + return ThemeSettings.values[prefs?.getInt('theme') ?? + ThemeSettings.system.index]; } set theme(ThemeSettings t) { @@ -123,8 +123,8 @@ class SettingsProvider with ChangeNotifier { } SortColumnSettings get sortColumn { - return SortColumnSettings.values[ - prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index]; + return SortColumnSettings.values[prefs?.getInt('sortColumn') ?? + SortColumnSettings.nameAuthor.index]; } set sortColumn(SortColumnSettings s) { @@ -133,8 +133,8 @@ class SettingsProvider with ChangeNotifier { } SortOrderSettings get sortOrder { - return SortOrderSettings.values[ - prefs?.getInt('sortOrder') ?? SortOrderSettings.ascending.index]; + return SortOrderSettings.values[prefs?.getInt('sortOrder') ?? + SortOrderSettings.ascending.index]; } set sortOrder(SortOrderSettings s) { @@ -171,7 +171,9 @@ class SettingsProvider with ChangeNotifier { while (!(await Permission.requestInstallPackages.isGranted)) { // Explicit request as InstallPlugin request sometimes bugged Fluttertoast.showToast( - msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG); + msg: tr('pleaseAllowInstallPerm'), + toastLength: Toast.LENGTH_LONG, + ); if ((await Permission.requestInstallPackages.request()) == PermissionStatus.granted) { return true; @@ -470,7 +472,8 @@ class SettingsProvider with ChangeNotifier { } List get searchDeselected { - return prefs?.getStringList('searchDeselected') ?? SourceProvider().sources.map((s) => s.name).toList(); + return prefs?.getStringList('searchDeselected') ?? + SourceProvider().sources.map((s) => s.name).toList(); } set searchDeselected(List list) { diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 72d88cd..9c6a14d 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -53,8 +53,14 @@ class APKDetails { late String? changeLog; late List> allAssetUrls; - APKDetails(this.version, this.apkUrls, this.names, - {this.releaseDate, this.changeLog, this.allAssetUrls = const []}); + APKDetails( + this.version, + this.apkUrls, + this.names, { + this.releaseDate, + this.changeLog, + this.allAssetUrls = const [], + }); } stringMapListTo2DList(List> mapList) => @@ -66,16 +72,21 @@ assumed2DlistToStringMapList(List arr) => // App JSON schema has changed multiple times over the many versions of Obtainium // This function takes an App JSON and modifies it if needed to conform to the latest (current) version appJSONCompatibilityModifiers(Map json) { - var source = SourceProvider() - .getSource(json['url'], overrideSource: json['overrideSource']); - var formItems = source.combinedAppSpecificSettingFormItems - .reduce((value, element) => [...value, ...element]); - Map additionalSettings = - getDefaultValuesFromFormItems([formItems]); + var source = SourceProvider().getSource( + json['url'], + overrideSource: json['overrideSource'], + ); + var formItems = source.combinedAppSpecificSettingFormItems.reduce( + (value, element) => [...value, ...element], + ); + Map additionalSettings = getDefaultValuesFromFormItems([ + formItems, + ]); Map originalAdditionalSettings = {}; if (json['additionalSettings'] != null) { - originalAdditionalSettings = - Map.from(jsonDecode(json['additionalSettings'])); + originalAdditionalSettings = Map.from( + jsonDecode(json['additionalSettings']), + ); additionalSettings.addEntries(originalAdditionalSettings.entries); } // If needed, migrate old-style additionalData to newer-style additionalSettings (V1) @@ -127,12 +138,14 @@ appJSONCompatibilityModifiers(Map json) { // Ensure additionalSettings are correctly typed for (var item in formItems) { if (additionalSettings[item.key] != null) { - additionalSettings[item.key] = - item.ensureType(additionalSettings[item.key]); + additionalSettings[item.key] = item.ensureType( + additionalSettings[item.key], + ); } } - int preferredApkIndex = - json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int; + int preferredApkIndex = json['preferredApkIndex'] == null + ? 0 + : json['preferredApkIndex'] as int; if (preferredApkIndex < 0) { preferredApkIndex = 0; } @@ -145,9 +158,9 @@ appJSONCompatibilityModifiers(Map json) { apkUrls = getApkUrlsFromUrls(List.from(apkUrlJson)); } catch (e) { apkUrls = assumed2DlistToStringMapList(List.from(apkUrlJson)); - apkUrls = List.from(apkUrlJson) - .map((e) => MapEntry(e[0] as String, e[1] as String)) - .toList(); + apkUrls = List.from( + apkUrlJson, + ).map((e) => MapEntry(e[0] as String, e[1] as String)).toList(); } json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls)); } @@ -173,22 +186,23 @@ appJSONCompatibilityModifiers(Map json) { 'customLinkFilterRegex': originalAdditionalSettings['intermediateLinkRegex'], 'filterByLinkText': - originalAdditionalSettings['intermediateLinkByText'] - } + originalAdditionalSettings['intermediateLinkByText'], + }, ]; } if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) { additionalSettings['intermediateLink'] = additionalSettings['intermediateLink'].where((e) { - return e['customLinkFilterRegex']?.isNotEmpty == true; - }).toList(); + return e['customLinkFilterRegex']?.isNotEmpty == true; + }).toList(); } // Steam source apps should be converted to HTML (#1244) var legacySteamSourceApps = ['steam', 'steam-chat-app']; if (legacySteamSourceApps.contains(additionalSettings['app'] ?? '')) { json['url'] = '${json['url']}/mobile'; var replacementAdditionalSettings = getDefaultValuesFromFormItems( - HTML().combinedAppSpecificSettingFormItems); + HTML().combinedAppSpecificSettingFormItems, + ); for (var s in replacementAdditionalSettings.keys) { if (additionalSettings.containsKey(s)) { replacementAdditionalSettings[s] = additionalSettings[s]; @@ -212,7 +226,8 @@ appJSONCompatibilityModifiers(Map json) { json['lastUpdateCheck'] != null) { json['url'] = 'https://updates.signal.org/android/latest.json'; var replacementAdditionalSettings = getDefaultValuesFromFormItems( - HTML().combinedAppSpecificSettingFormItems); + HTML().combinedAppSpecificSettingFormItems, + ); replacementAdditionalSettings['versionExtractionRegEx'] = '\\d+.\\d+.\\d+'; additionalSettings = replacementAdditionalSettings; @@ -228,7 +243,8 @@ appJSONCompatibilityModifiers(Map json) { json['lastUpdateCheck'] != null) { json['url'] = 'https://whatsapp.com/android'; var replacementAdditionalSettings = getDefaultValuesFromFormItems( - HTML().combinedAppSpecificSettingFormItems); + HTML().combinedAppSpecificSettingFormItems, + ); replacementAdditionalSettings['refreshBeforeDownload'] = true; additionalSettings = replacementAdditionalSettings; } @@ -243,25 +259,26 @@ appJSONCompatibilityModifiers(Map json) { json['lastUpdateCheck'] != null) { json['url'] = 'https://www.videolan.org/vlc/download-android.html'; var replacementAdditionalSettings = getDefaultValuesFromFormItems( - HTML().combinedAppSpecificSettingFormItems); + HTML().combinedAppSpecificSettingFormItems, + ); replacementAdditionalSettings['refreshBeforeDownload'] = true; replacementAdditionalSettings['intermediateLink'] = >[ - { - 'customLinkFilterRegex': 'APK', - 'filterByLinkText': true, - 'skipSort': false, - 'reverseSort': false, - 'sortByLastLinkSegment': false - }, - { - 'customLinkFilterRegex': 'arm64-v8a\\.apk\$', - 'filterByLinkText': false, - 'skipSort': false, - 'reverseSort': false, - 'sortByLastLinkSegment': false - } - ]; + { + 'customLinkFilterRegex': 'APK', + 'filterByLinkText': true, + 'skipSort': false, + 'reverseSort': false, + 'sortByLastLinkSegment': false, + }, + { + 'customLinkFilterRegex': 'arm64-v8a\\.apk\$', + 'filterByLinkText': false, + 'skipSort': false, + 'reverseSort': false, + 'sortByLastLinkSegment': false, + }, + ]; replacementAdditionalSettings['versionExtractionRegEx'] = '/vlc-android/([^/]+)/'; replacementAdditionalSettings['matchGroupToUse'] = "1"; @@ -277,8 +294,9 @@ appJSONCompatibilityModifiers(Map json) { json['overrideSource'] = FDroid().runtimeType.toString(); } else if (overrideSourceWasUndefined) { // Similar to above, but for third-party F-Droid repos - RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)') - .firstMatch(json['url'] as String); + RegExpMatch? match = RegExp( + '^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)', + ).firstMatch(json['url'] as String); if (match != null) { json['overrideSource'] = FDroidRepo().runtimeType.toString(); } @@ -305,23 +323,24 @@ class App { late String? overrideSource; bool allowIdChange = false; App( - this.id, - this.url, - this.author, - this.name, - this.installedVersion, - this.latestVersion, - this.apkUrls, - this.preferredApkIndex, - this.additionalSettings, - this.lastUpdateCheck, - this.pinned, - {this.categories = const [], - this.releaseDate, - this.changeLog, - this.overrideSource, - this.allowIdChange = false, - this.otherAssetUrls = const []}); + this.id, + this.url, + this.author, + this.name, + this.installedVersion, + this.latestVersion, + this.apkUrls, + this.preferredApkIndex, + this.additionalSettings, + this.lastUpdateCheck, + this.pinned, { + this.categories = const [], + this.releaseDate, + this.changeLog, + this.overrideSource, + this.allowIdChange = false, + this.otherAssetUrls = const [], + }); @override String toString() { @@ -330,8 +349,8 @@ class App { String? get overrideName => additionalSettings['appName']?.toString().trim().isNotEmpty == true - ? additionalSettings['appName'] - : null; + ? additionalSettings['appName'] + : null; String get finalName { return overrideName ?? name; @@ -339,31 +358,32 @@ class App { String? get overrideAuthor => additionalSettings['appAuthor']?.toString().trim().isNotEmpty == true - ? additionalSettings['appAuthor'] - : null; + ? additionalSettings['appAuthor'] + : null; String get finalAuthor { return overrideAuthor ?? author; } App deepCopy() => App( - id, - url, - author, - name, - installedVersion, - latestVersion, - apkUrls, - preferredApkIndex, - Map.from(additionalSettings), - lastUpdateCheck, - pinned, - categories: categories, - changeLog: changeLog, - releaseDate: releaseDate, - overrideSource: overrideSource, - allowIdChange: allowIdChange, - otherAssetUrls: otherAssetUrls); + id, + url, + author, + name, + installedVersion, + latestVersion, + apkUrls, + preferredApkIndex, + Map.from(additionalSettings), + lastUpdateCheck, + pinned, + categories: categories, + changeLog: changeLog, + releaseDate: releaseDate, + overrideSource: overrideSource, + allowIdChange: allowIdChange, + otherAssetUrls: otherAssetUrls, + ); factory App.fromJson(Map json) { Map originalJSON = Map.from(json); @@ -372,7 +392,8 @@ class App { } catch (e) { json = originalJSON; LogsProvider().add( - 'Error running JSON compat modifiers: ${e.toString()}: ${originalJSON.toString()}'); + 'Error running JSON compat modifiers: ${e.toString()}: ${originalJSON.toString()}', + ); } return App( json['id'] as String, @@ -384,7 +405,8 @@ class App { : json['installedVersion'] as String, (json['latestVersion'] ?? tr('unknown')) as String, assumed2DlistToStringMapList( - jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))), + jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]')), + ), (json['preferredApkIndex'] ?? -1) as int, jsonDecode(json['additionalSettings']) as Map, json['lastUpdateCheck'] == null @@ -393,11 +415,11 @@ class App { json['pinned'] ?? false, categories: json['categories'] != null ? (json['categories'] as List) - .map((e) => e.toString()) - .toList() + .map((e) => e.toString()) + .toList() : json['category'] != null - ? [json['category'] as String] - : [], + ? [json['category'] as String] + : [], releaseDate: json['releaseDate'] == null ? null : DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']), @@ -405,29 +427,30 @@ class App { overrideSource: json['overrideSource'], allowIdChange: json['allowIdChange'] ?? false, otherAssetUrls: assumed2DlistToStringMapList( - jsonDecode((json['otherAssetUrls'] ?? '[]'))), + jsonDecode((json['otherAssetUrls'] ?? '[]')), + ), ); } Map toJson() => { - 'id': id, - 'url': url, - 'author': author, - 'name': name, - 'installedVersion': installedVersion, - 'latestVersion': latestVersion, - 'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)), - 'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)), - 'preferredApkIndex': preferredApkIndex, - 'additionalSettings': jsonEncode(additionalSettings), - 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, - 'pinned': pinned, - 'categories': categories, - 'releaseDate': releaseDate?.microsecondsSinceEpoch, - 'changeLog': changeLog, - 'overrideSource': overrideSource, - 'allowIdChange': allowIdChange - }; + 'id': id, + 'url': url, + 'author': author, + 'name': name, + 'installedVersion': installedVersion, + 'latestVersion': latestVersion, + 'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)), + 'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)), + 'preferredApkIndex': preferredApkIndex, + 'additionalSettings': jsonEncode(additionalSettings), + 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, + 'pinned': pinned, + 'categories': categories, + 'releaseDate': releaseDate?.microsecondsSinceEpoch, + 'changeLog': changeLog, + 'overrideSource': overrideSource, + 'allowIdChange': allowIdChange, + }; } // Ensure the input is starts with HTTPS and has no WWW @@ -441,11 +464,13 @@ preStandardizeUrl(String url) { url = 'https://$url'; } var uri = Uri.tryParse(url); - var trailingSlash = ((uri?.path.endsWith('/') ?? false) || + var trailingSlash = + ((uri?.path.endsWith('/') ?? false) || ((uri?.path.isEmpty ?? false) && url.endsWith('/'))) && (uri?.queryParameters.isEmpty ?? false); - url = url + url = + url .split('/') .where((e) => e.isNotEmpty) .join('/') @@ -457,21 +482,26 @@ preStandardizeUrl(String url) { String noAPKFound = tr('noAPKFound'); List getLinksFromParsedHTML( - Document dom, RegExp hrefPattern, String prependToLinks) => - dom - .querySelectorAll('a') - .where((element) { - if (element.attributes['href'] == null) return false; - return hrefPattern.hasMatch(element.attributes['href']!); - }) - .map((e) => '$prependToLinks${e.attributes['href']!}') - .toList(); + Document dom, + RegExp hrefPattern, + String prependToLinks, +) => dom + .querySelectorAll('a') + .where((element) { + if (element.attributes['href'] == null) return false; + return hrefPattern.hasMatch(element.attributes['href']!); + }) + .map((e) => '$prependToLinks${e.attributes['href']!}') + .toList(); Map getDefaultValuesFromFormItems( - List> items) { - return Map.fromEntries(items - .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) - .reduce((value, element) => [...value, ...element])); + List> items, +) { + return Map.fromEntries( + items + .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) + .reduce((value, element) => [...value, ...element]), + ); } List> getApkUrlsFromUrls(List urls) => @@ -482,7 +512,8 @@ List> getApkUrlsFromUrls(List urls) => }).toList(); Future>> filterApksByArch( - List> apkUrls) async { + List> apkUrls, +) async { if (apkUrls.length > 1) { var abis = (await DeviceInfoPlugin().androidInfo).supportedAbis; for (var abi in abis) { @@ -511,20 +542,23 @@ HttpClient createHttpClient(bool insecure) { return client; } -Future>> sourceRequestStreamResponse( - String method, - String url, - Map? requestHeaders, - Map additionalSettings, - {bool followRedirects = true, - Object? postBody}) async { +Future>> +sourceRequestStreamResponse( + String method, + String url, + Map? requestHeaders, + Map additionalSettings, { + bool followRedirects = true, + Object? postBody, +}) async { var currentUrl = Uri.parse(url); var redirectCount = 0; const maxRedirects = 10; List cookies = []; while (redirectCount < maxRedirects) { - var httpClient = - createHttpClient(additionalSettings['allowInsecure'] == true); + var httpClient = createHttpClient( + additionalSettings['allowInsecure'] == true, + ); var request = await httpClient.openUrl(method, currentUrl); if (requestHeaders != null) { requestHeaders.forEach((key, value) { @@ -556,11 +590,16 @@ Future>> sourceRequestStr throw ObtainiumError('Too many redirects ($maxRedirects)'); } -Future httpClientResponseStreamToFinalResponse(HttpClient httpClient, - String method, String url, HttpClientResponse response) async { - final bytes = - (await response.fold(BytesBuilder(), (b, d) => b..add(d))) - .toBytes(); +Future httpClientResponseStreamToFinalResponse( + HttpClient httpClient, + String method, + String url, + HttpClientResponse response, +) async { + final bytes = (await response.fold( + BytesBuilder(), + (b, d) => b..add(d), + )).toBytes(); final headers = {}; response.headers.forEach((name, values) { @@ -598,20 +637,24 @@ abstract class AppSource { name = runtimeType.toString(); } - overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(String key, - {bool disabled = true, bool defaultValue = true}) { + overrideAdditionalAppSpecificSourceAgnosticSettingSwitch( + String key, { + bool disabled = true, + bool defaultValue = true, + }) { additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = - additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly - .map((e) { - return e.map((e2) { - if (e2.key == key) { - var item = e2 as GeneratedFormSwitch; - item.disabled = disabled; - item.defaultValue = defaultValue; - } - return e2; - }).toList(); - }).toList(); + additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.map( + (e) { + return e.map((e2) { + if (e2.key == key) { + var item = e2 as GeneratedFormSwitch; + item.disabled = disabled; + item.defaultValue = defaultValue; + } + return e2; + }).toList(); + }, + ).toList(); } String standardizeUrl(String url) { @@ -623,8 +666,9 @@ abstract class AppSource { } Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { + Map additionalSettings, { + bool forAPKDownload = false, + }) async { return null; } @@ -633,18 +677,28 @@ abstract class AppSource { } Future sourceRequest( - String url, Map additionalSettings, - {bool followRedirects = true, Object? postBody}) async { + String url, + Map additionalSettings, { + bool followRedirects = true, + Object? postBody, + }) async { var method = postBody == null ? 'GET' : 'POST'; var requestHeaders = await getRequestHeaders(additionalSettings); - var streamedResponseUrlWithResponseAndClient = await sourceRequestStreamResponse( - method, url, requestHeaders, additionalSettings, - followRedirects: followRedirects, postBody: postBody); + var streamedResponseUrlWithResponseAndClient = + await sourceRequestStreamResponse( + method, + url, + requestHeaders, + additionalSettings, + followRedirects: followRedirects, + postBody: postBody, + ); return await httpClientResponseStreamToFinalResponse( - streamedResponseUrlWithResponseAndClient.value.key, - method, - streamedResponseUrlWithResponseAndClient.key.toString(), - streamedResponseUrlWithResponseAndClient.value.value); + streamedResponseUrlWithResponseAndClient.value.key, + method, + streamedResponseUrlWithResponseAndClient.key.toString(), + streamedResponseUrlWithResponseAndClient.value.value, + ); } void runOnAddAppInputChange(String inputUrl) { @@ -656,7 +710,9 @@ abstract class AppSource { } Future getLatestAPKDetails( - String standardUrl, Map additionalSettings) { + String standardUrl, + Map additionalSettings, + ) { throw NotImplementedError(); } @@ -666,121 +722,160 @@ abstract class AppSource { // Some additional data may be needed for Apps regardless of Source List> - additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [ + additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [ + [GeneratedFormSwitch('trackOnly', label: tr('trackOnly'))], + [ + GeneratedFormTextField( + 'versionExtractionRegEx', + label: tr('trimVersionString'), + required: false, + additionalValidators: [(value) => regExValidator(value)], + ), + ], + [ + GeneratedFormTextField( + 'matchGroupToUse', + label: tr('matchGroupToUseForX', args: [tr('trimVersionString')]), + required: false, + hint: '\$0', + ), + ], [ GeneratedFormSwitch( - 'trackOnly', - label: tr('trackOnly'), - ) + 'versionDetection', + label: tr('versionDetectionExplanation'), + defaultValue: true, + ), ], [ - GeneratedFormTextField('versionExtractionRegEx', - label: tr('trimVersionString'), - required: false, - additionalValidators: [(value) => regExValidator(value)]), + GeneratedFormSwitch( + 'useVersionCodeAsOSVersion', + label: tr('useVersionCodeAsOSVersion'), + defaultValue: false, + ), ], [ - GeneratedFormTextField('matchGroupToUse', - label: tr('matchGroupToUseForX', args: [tr('trimVersionString')]), - required: false, - hint: '\$0') + GeneratedFormTextField( + 'apkFilterRegEx', + label: tr('filterAPKsByRegEx'), + required: false, + additionalValidators: [ + (value) { + return regExValidator(value); + }, + ], + ), ], [ - GeneratedFormSwitch('versionDetection', - label: tr('versionDetectionExplanation'), defaultValue: true) + GeneratedFormSwitch( + 'invertAPKFilter', + label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})', + defaultValue: false, + ), ], [ - GeneratedFormSwitch('useVersionCodeAsOSVersion', - label: tr('useVersionCodeAsOSVersion'), defaultValue: false) - ], - [ - GeneratedFormTextField('apkFilterRegEx', - label: tr('filterAPKsByRegEx'), - required: false, - additionalValidators: [ - (value) { - return regExValidator(value); - } - ]) - ], - [ - GeneratedFormSwitch('invertAPKFilter', - label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})', - defaultValue: false) - ], - [ - GeneratedFormSwitch('autoApkFilterByArch', - label: tr('autoApkFilterByArch'), defaultValue: true) + GeneratedFormSwitch( + 'autoApkFilterByArch', + label: tr('autoApkFilterByArch'), + defaultValue: true, + ), ], [GeneratedFormTextField('appName', label: tr('appName'), required: false)], [GeneratedFormTextField('appAuthor', label: tr('author'), required: false)], [ - GeneratedFormSwitch('shizukuPretendToBeGooglePlay', - label: tr('shizukuPretendToBeGooglePlay'), defaultValue: false) + GeneratedFormSwitch( + 'shizukuPretendToBeGooglePlay', + label: tr('shizukuPretendToBeGooglePlay'), + defaultValue: false, + ), ], [ - GeneratedFormSwitch('allowInsecure', - label: tr('allowInsecure'), defaultValue: false) + GeneratedFormSwitch( + 'allowInsecure', + label: tr('allowInsecure'), + defaultValue: false, + ), ], [ - GeneratedFormSwitch('exemptFromBackgroundUpdates', - label: tr('exemptFromBackgroundUpdates')) + GeneratedFormSwitch( + 'exemptFromBackgroundUpdates', + label: tr('exemptFromBackgroundUpdates'), + ), ], [ - GeneratedFormSwitch('skipUpdateNotifications', - label: tr('skipUpdateNotifications')) + GeneratedFormSwitch( + 'skipUpdateNotifications', + label: tr('skipUpdateNotifications'), + ), ], [GeneratedFormTextField('about', label: tr('about'), required: false)], [ - GeneratedFormSwitch('refreshBeforeDownload', - label: tr('refreshBeforeDownload')) - ] + GeneratedFormSwitch( + 'refreshBeforeDownload', + label: tr('refreshBeforeDownload'), + ), + ], ]; // Previous 2 variables combined into one at runtime for convenient usage List> get combinedAppSpecificSettingFormItems { if (showReleaseDateAsVersionToggle == true) { if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly - .indexWhere((List e) => - e.indexWhere((GeneratedFormItem i) => - i.key == 'releaseDateAsVersion') >= - 0) < + .indexWhere( + (List e) => + e.indexWhere( + (GeneratedFormItem i) => i.key == 'releaseDateAsVersion', + ) >= + 0, + ) < 0) { - additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.insert( - additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly - .indexWhere((List e) => - e.indexWhere((GeneratedFormItem i) => - i.key == 'versionDetection') >= - 0) + - 1, - [ - GeneratedFormSwitch('releaseDateAsVersion', + additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly + .insert( + additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly + .indexWhere( + (List e) => + e.indexWhere( + (GeneratedFormItem i) => + i.key == 'versionDetection', + ) >= + 0, + ) + + 1, + [ + GeneratedFormSwitch( + 'releaseDateAsVersion', label: '${tr('releaseDateAsVersion')} (${tr('pseudoVersion')})', - defaultValue: false) - ]); + defaultValue: false, + ), + ], + ); } } additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly - .map((e) => e - .where((ee) => !excludeCommonSettingKeys.contains(ee.key)) - .toList()) + .map( + (e) => e + .where((ee) => !excludeCommonSettingKeys.contains(ee.key)) + .toList(), + ) .where((e) => e.isNotEmpty) .toList(); if (versionDetectionDisallowed) { overrideAdditionalAppSpecificSourceAgnosticSettingSwitch( - 'versionDetection', - disabled: true, - defaultValue: false); + 'versionDetection', + disabled: true, + defaultValue: false, + ); overrideAdditionalAppSpecificSourceAgnosticSettingSwitch( - 'useVersionCodeAsOSVersion', - disabled: true, - defaultValue: false); + 'useVersionCodeAsOSVersion', + disabled: true, + defaultValue: false, + ); } return [ ...additionalSourceAppSpecificSettingFormItems, - ...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly + ...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly, ]; } @@ -788,14 +883,15 @@ abstract class AppSource { // If the source has been overridden, we expect the user to define one-time values as additional settings - don't use the stored values List sourceConfigSettingFormItems = []; Future> getSourceConfigValues( - Map additionalSettings, - SettingsProvider settingsProvider) async { + Map additionalSettings, + SettingsProvider settingsProvider, + ) async { Map results = {}; for (var e in sourceConfigSettingFormItems) { var val = hostChanged && !hostIdenticalDespiteAnyChange ? additionalSettings[e.key] : additionalSettings[e.key] ?? - settingsProvider.getSettingString(e.key); + settingsProvider.getSettingString(e.key); if (val != null) { results[e.key] = val; } @@ -811,31 +907,40 @@ abstract class AppSource { return null; } - Future apkUrlPrefetchModifier(String apkUrl, String standardUrl, - Map additionalSettings) async { + Future apkUrlPrefetchModifier( + String apkUrl, + String standardUrl, + Map additionalSettings, + ) async { return apkUrl; } bool canSearch = false; bool includeAdditionalOptsInMainSearch = false; List searchQuerySettingFormItems = []; - Future>> search(String query, - {Map querySettings = const {}}) { + Future>> search( + String query, { + Map querySettings = const {}, + }) { throw NotImplementedError(); } - Future tryInferringAppId(String standardUrl, - {Map additionalSettings = const {}}) async { + Future tryInferringAppId( + String standardUrl, { + Map additionalSettings = const {}, + }) async { return null; } } ObtainiumError getObtainiumHttpError(Response res) { - return ObtainiumError((res.reasonPhrase != null && - res.reasonPhrase != null && - res.reasonPhrase!.isNotEmpty) - ? res.reasonPhrase! - : tr('errorWithHttpStatusCode', args: [res.statusCode.toString()])); + return ObtainiumError( + (res.reasonPhrase != null && + res.reasonPhrase != null && + res.reasonPhrase!.isNotEmpty) + ? res.reasonPhrase! + : tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]), + ); } abstract class MassAppUrlSource { @@ -904,8 +1009,11 @@ replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) { return outputString; } -String? extractVersion(String? versionExtractionRegEx, String? matchGroupString, - String stringToCheck) { +String? extractVersion( + String? versionExtractionRegEx, + String? matchGroupString, + String stringToCheck, +) { if (versionExtractionRegEx?.isNotEmpty == true) { String? version = stringToCheck; var match = RegExp(versionExtractionRegEx!).allMatches(version); @@ -927,9 +1035,10 @@ String? extractVersion(String? versionExtractionRegEx, String? matchGroupString, } List> filterApks( - List> apkUrls, - String? apkFilterRegEx, - bool? invert) { + List> apkUrls, + String? apkFilterRegEx, + bool? invert, +) { if (apkFilterRegEx?.isNotEmpty == true) { var reg = RegExp(apkFilterRegEx!); apkUrls = apkUrls.where((element) { @@ -941,35 +1050,35 @@ List> filterApks( } isVersionPseudo(App app) => - app.additionalSettings['trackOnly'] == true || - (app.installedVersion != null && - app.additionalSettings['versionDetection'] != true); + app.additionalSettings['trackOnly'] == true || + (app.installedVersion != null && + app.additionalSettings['versionDetection'] != true); class SourceProvider { // Add more source classes here so they are available via the service List get sources => [ - GitHub(), - GitLab(), - Codeberg(), - FDroid(), - FDroidRepo(), - IzzyOnDroid(), - SourceHut(), - APKPure(), - Aptoide(), - Uptodown(), - HuaweiAppGallery(), - Tencent(), - CoolApk(), - VivoAppStore(), - Jenkins(), - APKMirror(), - RuStore(), - TelegramApp(), - NeutronCode(), - DirectAPKLink(), - HTML() // This should ALWAYS be the last option as they are tried in order - ]; + GitHub(), + GitLab(), + Codeberg(), + FDroid(), + FDroidRepo(), + IzzyOnDroid(), + SourceHut(), + APKPure(), + Aptoide(), + Uptodown(), + HuaweiAppGallery(), + Tencent(), + CoolApk(), + VivoAppStore(), + Jenkins(), + APKMirror(), + RuStore(), + TelegramApp(), + NeutronCode(), + DirectAPKLink(), + HTML(), // This should ALWAYS be the last option as they are tried in order + ]; // Add more mass url source classes here so they are available via the service List massUrlSources = [GitHubStars()]; @@ -977,8 +1086,9 @@ class SourceProvider { AppSource getSource(String url, {String? overrideSource}) { url = preStandardizeUrl(url); if (overrideSource != null) { - var srcs = - sources.where((e) => e.runtimeType.toString() == overrideSource); + var srcs = sources.where( + (e) => e.runtimeType.toString() == overrideSource, + ); if (srcs.isEmpty) { throw UnsupportedURLError(); } @@ -996,8 +1106,8 @@ class SourceProvider { for (var s in sources.where((element) => element.hosts.isNotEmpty)) { try { if (RegExp( - '^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$') - .hasMatch(Uri.parse(url).host)) { + '^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$', + ).hasMatch(Uri.parse(url).host)) { source = s; break; } @@ -1007,7 +1117,8 @@ class SourceProvider { } if (source == null) { for (var s in sources.where( - (element) => element.hosts.isEmpty && !element.neverAutoSelect)) { + (element) => element.hosts.isEmpty && !element.neverAutoSelect, + )) { try { s.sourceSpecificStandardizeURL(url, forSelection: true); source = s; @@ -1035,30 +1146,37 @@ class SourceProvider { } String generateTempID( - String standardUrl, Map additionalSettings) => - (standardUrl + additionalSettings.toString()).hashCode.toString(); + String standardUrl, + Map additionalSettings, + ) => (standardUrl + additionalSettings.toString()).hashCode.toString(); Future getApp( - AppSource source, String url, Map additionalSettings, - {App? currentApp, - bool trackOnlyOverride = false, - bool sourceIsOverriden = false, - bool inferAppIdIfOptional = false}) async { + AppSource source, + String url, + Map additionalSettings, { + App? currentApp, + bool trackOnlyOverride = false, + bool sourceIsOverriden = false, + bool inferAppIdIfOptional = false, + }) async { if (trackOnlyOverride || source.enforceTrackOnly) { additionalSettings['trackOnly'] = true; } var trackOnly = additionalSettings['trackOnly'] == true; String standardUrl = source.standardizeUrl(url); - APKDetails apk = - await source.getLatestAPKDetails(standardUrl, additionalSettings); + APKDetails apk = await source.getLatestAPKDetails( + standardUrl, + additionalSettings, + ); if (source.runtimeType != HTML().runtimeType && // Some sources do it separately source.runtimeType != SourceForge().runtimeType) { String? extractedVersion = extractVersion( - additionalSettings['versionExtractionRegEx'] as String?, - additionalSettings['matchGroupToUse'] as String?, - apk.version); + additionalSettings['versionExtractionRegEx'] as String?, + additionalSettings['matchGroupToUse'] as String?, + apk.version, + ); if (extractedVersion != null) { apk.version = extractedVersion; } @@ -1068,8 +1186,11 @@ class SourceProvider { apk.releaseDate != null) { apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString(); } - apk.apkUrls = filterApks(apk.apkUrls, additionalSettings['apkFilterRegEx'], - additionalSettings['invertAPKFilter']); + apk.apkUrls = filterApks( + apk.apkUrls, + additionalSettings['apkFilterRegEx'], + additionalSettings['invertAPKFilter'], + ); if (apk.apkUrls.isEmpty && !trackOnly) { throw NoAPKError(); } @@ -1079,47 +1200,53 @@ class SourceProvider { var name = currentApp != null ? currentApp.name.trim() : ''; name = name.isNotEmpty ? name : apk.names.name; App finalApp = App( - currentApp?.id ?? - ((additionalSettings['appId'] != null) - ? additionalSettings['appId'] - : null) ?? - (!trackOnly && - (!source.appIdInferIsOptional || - (source.appIdInferIsOptional && inferAppIdIfOptional)) - ? await source.tryInferringAppId(standardUrl, - additionalSettings: additionalSettings) - : null) ?? - generateTempID(standardUrl, additionalSettings), - standardUrl, - apk.names.author, - name, - currentApp?.installedVersion, - apk.version, - apk.apkUrls, - apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0, - additionalSettings, - DateTime.now(), - currentApp?.pinned ?? false, - categories: currentApp?.categories ?? const [], - releaseDate: apk.releaseDate, - changeLog: apk.changeLog, - overrideSource: sourceIsOverriden - ? source.runtimeType.toString() - : currentApp?.overrideSource, - allowIdChange: currentApp?.allowIdChange ?? - trackOnly || - (source.appIdInferIsOptional && - inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install - otherAssetUrls: apk.allAssetUrls - .where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0) - .toList()); + currentApp?.id ?? + ((additionalSettings['appId'] != null) + ? additionalSettings['appId'] + : null) ?? + (!trackOnly && + (!source.appIdInferIsOptional || + (source.appIdInferIsOptional && inferAppIdIfOptional)) + ? await source.tryInferringAppId( + standardUrl, + additionalSettings: additionalSettings, + ) + : null) ?? + generateTempID(standardUrl, additionalSettings), + standardUrl, + apk.names.author, + name, + currentApp?.installedVersion, + apk.version, + apk.apkUrls, + apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0, + additionalSettings, + DateTime.now(), + currentApp?.pinned ?? false, + categories: currentApp?.categories ?? const [], + releaseDate: apk.releaseDate, + changeLog: apk.changeLog, + overrideSource: sourceIsOverriden + ? source.runtimeType.toString() + : currentApp?.overrideSource, + allowIdChange: + currentApp?.allowIdChange ?? + trackOnly || + (source.appIdInferIsOptional && + inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install + otherAssetUrls: apk.allAssetUrls + .where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0) + .toList(), + ); return source.endOfGetAppChanges(finalApp); } // Returns errors in [results, errors] instead of throwing them - Future> getAppsByURLNaive(List urls, - {List alreadyAddedUrls = const [], - AppSource? sourceOverride}) async { + Future> getAppsByURLNaive( + List urls, { + List alreadyAddedUrls = const [], + AppSource? sourceOverride, + }) async { List apps = []; Map errors = {}; for (var url in urls) { @@ -1128,12 +1255,16 @@ class SourceProvider { throw ObtainiumError(tr('appAlreadyAdded')); } var source = sourceOverride ?? getSource(url); - apps.add(await getApp( + apps.add( + await getApp( source, url, sourceIsOverriden: sourceOverride != null, getDefaultValuesFromFormItems( - source.combinedAppSpecificSettingFormItems))); + source.combinedAppSpecificSettingFormItems, + ), + ), + ); } catch (e) { errors.addAll({url: e}); }