mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Merge pull request #2350 from ImranR98/dev
- Minor wording changes (#2226, #2313, #2317, #2338) - HTML bugfix: Incorrect URL resolution on redirected pages (#2315)
This commit is contained in:
2
.flutter
2
.flutter
Submodule .flutter updated: b25305a883...6fba2447e9
@ -63,7 +63,7 @@ Or, contribute some configurations to the website by creating a PR at [this repo
|
|||||||
|
|
||||||
Verification info:
|
Verification info:
|
||||||
- Package ID: `dev.imranr.obtainium`
|
- Package ID: `dev.imranr.obtainium`
|
||||||
- SHA-256 Hash of Signing Certificate: `B3:53:60:1F:6A:1D:5F:D6:60:3A:E2:F5:0B:E8:0C:F3:01:36:7B:86:B6:AB:8B:1F:66:24:3D:A9:6C:D5:73:62`
|
- SHA-256 hash of signing certificate: `B3:53:60:1F:6A:1D:5F:D6:60:3A:E2:F5:0B:E8:0C:F3:01:36:7B:86:B6:AB:8B:1F:66:24:3D:A9:6C:D5:73:62`
|
||||||
- Note: The above signature is also valid for the F-Droid flavour of Obtainium, thanks to [reproducible builds](https://f-droid.org/docs/Reproducible_Builds/).
|
- Note: The above signature is also valid for the F-Droid flavour of Obtainium, thanks to [reproducible builds](https://f-droid.org/docs/Reproducible_Builds/).
|
||||||
- [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) (to verify APK hashes)
|
- [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) (to verify APK hashes)
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@ class APKCombo extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -22,25 +23,30 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
return {
|
return {
|
||||||
"User-Agent": "curl/8.0.1",
|
"User-Agent": "curl/8.0.1",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Host": hosts[0]
|
"Host": hosts[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<MapEntry<String, String>>> getApkUrls(
|
Future<List<MapEntry<String, String>>> getApkUrls(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest('$standardUrl/download/apk', {});
|
var res = await sourceRequest('$standardUrl/download/apk', {});
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -65,7 +71,9 @@ class APKCombo extends AppSource {
|
|||||||
String verCode =
|
String verCode =
|
||||||
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
||||||
return MapEntry<String, String>(
|
return MapEntry<String, String>(
|
||||||
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
|
arch != null ? '$arch-$verCode.apk' : '',
|
||||||
|
url ?? '',
|
||||||
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
})
|
})
|
||||||
.reduce((value, element) => [...value, ...element])
|
.reduce((value, element) => [...value, ...element])
|
||||||
@ -74,8 +82,11 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
var freshURLs = await getApkUrls(standardUrl, additionalSettings);
|
||||||
var path2Match = Uri.parse(apkUrl).path;
|
var path2Match = Uri.parse(apkUrl).path;
|
||||||
for (var url in freshURLs) {
|
for (var url in freshURLs) {
|
||||||
@ -119,6 +130,7 @@ class APKCombo extends AppSource {
|
|||||||
version,
|
version,
|
||||||
await getApkUrls(standardUrl, additionalSettings),
|
await getApkUrls(standardUrl, additionalSettings),
|
||||||
AppNames(author, appName),
|
AppNames(author, appName),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,29 +17,35 @@ class APKMirror extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
GeneratedFormTextField(
|
||||||
|
'filterReleaseTitlesByRegEx',
|
||||||
label: tr('filterReleaseTitlesByRegEx'),
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
return {
|
return {
|
||||||
"User-Agent":
|
"User-Agent":
|
||||||
"Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}"
|
"Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +53,8 @@ class APKMirror extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/apk/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -72,8 +79,10 @@ class APKMirror extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('$standardUrl/feed/', additionalSettings);
|
'$standardUrl/feed/',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var items = parse(res.body).querySelectorAll('item');
|
var items = parse(res.body).querySelectorAll('item');
|
||||||
dynamic targetRelease;
|
dynamic targetRelease;
|
||||||
@ -95,11 +104,14 @@ class APKMirror extends AppSource {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.sublist(0, 5)
|
.sublist(0, 5)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
DateTime? releaseDate =
|
DateTime? releaseDate = dateString != null
|
||||||
dateString != null ? HttpDate.parse('$dateString GMT') : null;
|
? HttpDate.parse('$dateString GMT')
|
||||||
|
: null;
|
||||||
String? version = titleString
|
String? version = titleString
|
||||||
?.substring(RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
?.substring(
|
||||||
RegExp(' by ').allMatches(titleString).last.start)
|
RegExp('[0-9]').firstMatch(titleString)?.start ?? 0,
|
||||||
|
RegExp(' by ').allMatches(titleString).last.start,
|
||||||
|
)
|
||||||
.trim();
|
.trim();
|
||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
version = titleString;
|
version = titleString;
|
||||||
@ -107,8 +119,12 @@ class APKMirror extends AppSource {
|
|||||||
if (version == null || version.isEmpty) {
|
if (version == null || version.isEmpty) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, [], getAppNames(standardUrl),
|
return APKDetails(
|
||||||
releaseDate: releaseDate);
|
version,
|
||||||
|
[],
|
||||||
|
getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
extension Unique<E, Id> on List<E> {
|
extension Unique<E, Id> on List<E> {
|
||||||
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
|
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
|
||||||
final ids = Set();
|
final ids = <dynamic>{};
|
||||||
var list = inplace ? this : List<E>.from(this);
|
var list = inplace ? this : List<E>.from(this);
|
||||||
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
|
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
|
||||||
return list;
|
return list;
|
||||||
@ -23,17 +23,26 @@ class APKPure extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('stayOneVersionBehind',
|
GeneratedFormSwitch(
|
||||||
label: tr('stayOneVersionBehind'), defaultValue: false)
|
'stayOneVersionBehind',
|
||||||
|
label: tr('stayOneVersionBehind'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('useFirstApkOfVersion',
|
GeneratedFormSwitch(
|
||||||
label: tr('useFirstApkOfVersion'), defaultValue: true)
|
'useFirstApkOfVersion',
|
||||||
]
|
label: tr('useFirstApkOfVersion'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +50,8 @@ class APKPure extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
'^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
var uri = Uri.parse(url);
|
var uri = Uri.parse(url);
|
||||||
@ -49,7 +59,8 @@ class APKPure extends AppSource {
|
|||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -58,15 +69,18 @@ class APKPure extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDetailsForVersion(
|
Future<APKDetails> getDetailsForVersion(
|
||||||
List<Map<String, dynamic>> versionVariants,
|
List<Map<String, dynamic>> versionVariants,
|
||||||
List<String> supportedArchs,
|
List<String> supportedArchs,
|
||||||
Map<String, dynamic> additionalSettings) async {
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var apkUrls = versionVariants
|
var apkUrls = versionVariants
|
||||||
.map((e) {
|
.map((e) {
|
||||||
String appId = e['package_name'];
|
String appId = e['package_name'];
|
||||||
@ -89,7 +103,8 @@ class APKPure extends AppSource {
|
|||||||
|
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
'$appId-$versionCode-$architectureString.${type.toLowerCase()}',
|
'$appId-$versionCode-$architectureString.${type.toLowerCase()}',
|
||||||
downloadUri);
|
downloadUri,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.nonNulls
|
.nonNulls
|
||||||
.toList()
|
.toList()
|
||||||
@ -114,14 +129,20 @@ class APKPure extends AppSource {
|
|||||||
apkUrls = [apkUrls.first];
|
apkUrls = [apkUrls.first];
|
||||||
}
|
}
|
||||||
|
|
||||||
return APKDetails(version, apkUrls, AppNames(author, appName),
|
return APKDetails(
|
||||||
releaseDate: releaseDate, changeLog: changeLog);
|
version,
|
||||||
|
apkUrls,
|
||||||
|
AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
if (forAPKDownload) {
|
if (forAPKDownload) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
@ -146,18 +167,21 @@ class APKPure extends AppSource {
|
|||||||
// request versions from API
|
// request versions from API
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
"https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en",
|
"https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en",
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
List<Map<String, dynamic>> apks =
|
List<Map<String, dynamic>> apks = jsonDecode(
|
||||||
jsonDecode(res.body)['version_list'].cast<Map<String, dynamic>>();
|
res.body,
|
||||||
|
)['version_list'].cast<Map<String, dynamic>>();
|
||||||
|
|
||||||
// group by version
|
// group by version
|
||||||
List<List<Map<String, dynamic>>> versions = apks
|
List<List<Map<String, dynamic>>> versions = apks
|
||||||
.fold<Map<String, List<Map<String, dynamic>>>>({},
|
.fold<Map<String, List<Map<String, dynamic>>>>({}, (
|
||||||
(Map<String, List<Map<String, dynamic>>> val,
|
Map<String, List<Map<String, dynamic>>> val,
|
||||||
Map<String, dynamic> element) {
|
Map<String, dynamic> element,
|
||||||
|
) {
|
||||||
String v = element['version_name'];
|
String v = element['version_name'];
|
||||||
if (!val.containsKey(v)) {
|
if (!val.containsKey(v)) {
|
||||||
val[v] = [];
|
val[v] = [];
|
||||||
@ -179,7 +203,10 @@ class APKPure extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
return await getDetailsForVersion(
|
return await getDetailsForVersion(
|
||||||
v, supportedArchs, additionalSettings);
|
v,
|
||||||
|
supportedArchs,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (additionalSettings['fallbackToOlderReleases'] != true ||
|
if (additionalSettings['fallbackToOlderReleases'] != true ||
|
||||||
i == versions.length - 1) {
|
i == versions.length - 1) {
|
||||||
|
@ -17,7 +17,8 @@ class Aptoide extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -26,14 +27,20 @@ class Aptoide extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return (await getAppDetailsJSON(
|
return (await getAppDetailsJSON(
|
||||||
standardUrl, additionalSettings))['package'];
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
))['package'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getAppDetailsJSON(
|
Future<Map<String, dynamic>> getAppDetailsJSON(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -46,7 +53,9 @@ class Aptoide extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res2 = await sourceRequest(
|
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) {
|
if (res2.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -76,7 +85,10 @@ class Aptoide extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
version,
|
||||||
releaseDate: relDate);
|
getApkUrlsFromUrls([apkUrl]),
|
||||||
|
AppNames(author, appName),
|
||||||
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,8 @@ class Codeberg extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -36,8 +37,9 @@ class Codeberg extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings, (
|
||||||
(bool useTagUrl) async {
|
bool useTagUrl,
|
||||||
|
) async {
|
||||||
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
return 'https://${hosts[0]}/api/v1/repos${standardUrl.substring('https://${hosts[0]}'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
@ -49,12 +51,15 @@ class Codeberg extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
return gh.searchCommon(
|
return gh.searchCommon(
|
||||||
query,
|
query,
|
||||||
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
'https://${hosts[0]}/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||||
'data',
|
'data',
|
||||||
querySettings: querySettings);
|
querySettings: querySettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ class CoolApk extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
r'^https?://(www\.)?coolapk\.com/apk/[^/]+',
|
r'^https?://(www\.)?coolapk\.com/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -30,8 +31,10 @@ class CoolApk extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
String appId = Uri.parse(standardUrl).pathSegments.last;
|
String appId = Uri.parse(standardUrl).pathSegments.last;
|
||||||
return appId;
|
return appId;
|
||||||
}
|
}
|
||||||
@ -71,7 +74,13 @@ class CoolApk extends AppSource {
|
|||||||
String aid = detail['id'].toString();
|
String aid = detail['id'].toString();
|
||||||
|
|
||||||
// get apk url
|
// get apk url
|
||||||
String apkUrl = await _getLatestApkUrl(apiUrl, appId, aid, version, headers);
|
String apkUrl = await _getLatestApkUrl(
|
||||||
|
apiUrl,
|
||||||
|
appId,
|
||||||
|
aid,
|
||||||
|
version,
|
||||||
|
headers,
|
||||||
|
);
|
||||||
if (apkUrl.isEmpty) {
|
if (apkUrl.isEmpty) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
@ -89,8 +98,13 @@ class CoolApk extends AppSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getLatestApkUrl(String apiUrl, String appId, String aid,
|
Future<String> _getLatestApkUrl(
|
||||||
String version, Map<String, String>? headers) async {
|
String apiUrl,
|
||||||
|
String appId,
|
||||||
|
String aid,
|
||||||
|
String version,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
) async {
|
||||||
String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid';
|
String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid';
|
||||||
var res = await sourceRequest(url, {}, followRedirects: false);
|
var res = await sourceRequest(url, {}, followRedirects: false);
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400) {
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
||||||
@ -102,8 +116,9 @@ class CoolApk extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
var tokenPair = _getToken();
|
var tokenPair = _getToken();
|
||||||
// CoolAPK header
|
// CoolAPK header
|
||||||
return {
|
return {
|
||||||
@ -128,14 +143,15 @@ class CoolApk extends AppSource {
|
|||||||
Map<String, String> _getToken() {
|
Map<String, String> _getToken() {
|
||||||
final rand = Random();
|
final rand = Random();
|
||||||
|
|
||||||
String randHexString(int n) =>
|
String randHexString(int n) => List.generate(
|
||||||
List.generate(n, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'))
|
n,
|
||||||
.join()
|
(_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||||
.toUpperCase();
|
).join().toUpperCase();
|
||||||
|
|
||||||
String randMacAddress() =>
|
String randMacAddress() => List.generate(
|
||||||
List.generate(6, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'))
|
6,
|
||||||
.join(':');
|
(_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||||
|
).join(':');
|
||||||
|
|
||||||
// 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP
|
// 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP
|
||||||
// device
|
// device
|
||||||
@ -147,11 +163,13 @@ class CoolApk extends AppSource {
|
|||||||
const buildNumber = 'SQ1D.220105.007';
|
const buildNumber = 'SQ1D.220105.007';
|
||||||
|
|
||||||
// generate deviceCode
|
// generate deviceCode
|
||||||
String deviceCode =
|
String deviceCode = base64.encode(
|
||||||
base64.encode('$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits);
|
'$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits,
|
||||||
|
);
|
||||||
|
|
||||||
// generate timestamp
|
// generate timestamp
|
||||||
String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000)
|
||||||
|
.toString();
|
||||||
String base64TimeStamp = base64.encode(timeStamp.codeUnits);
|
String base64TimeStamp = base64.encode(timeStamp.codeUnits);
|
||||||
String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString();
|
String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString();
|
||||||
String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString();
|
String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString();
|
||||||
@ -164,7 +182,8 @@ class CoolApk extends AppSource {
|
|||||||
String md5Token = md5.convert(token.codeUnits).toString();
|
String md5Token = md5.convert(token.codeUnits).toString();
|
||||||
|
|
||||||
// generate salt and hash
|
// 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 bcryptResult = BCrypt.hashpw(md5Base64Token, bcryptSalt);
|
||||||
String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y');
|
String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y');
|
||||||
String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}';
|
String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}';
|
||||||
|
@ -11,20 +11,23 @@ class DirectAPKLink extends AppSource {
|
|||||||
name = tr('directAPKLink');
|
name = tr('directAPKLink');
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
...html.additionalSourceAppSpecificSettingFormItems
|
...html.additionalSourceAppSpecificSettingFormItems
|
||||||
.where((element) => element
|
.where(
|
||||||
|
(element) => element
|
||||||
.where((element) => element.key == 'requestHeader')
|
.where((element) => element.key == 'requestHeader')
|
||||||
.isNotEmpty)
|
.isNotEmpty,
|
||||||
.toList(),
|
)
|
||||||
|
,
|
||||||
[
|
[
|
||||||
GeneratedFormDropdown(
|
GeneratedFormDropdown(
|
||||||
'defaultPseudoVersioningMethod',
|
'defaultPseudoVersioningMethod',
|
||||||
[
|
[
|
||||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
MapEntry('ETag', 'ETag')
|
MapEntry('ETag', 'ETag'),
|
||||||
],
|
],
|
||||||
label: tr('defaultPseudoVersioningMethod'),
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
defaultValue: 'partialAPKHash')
|
defaultValue: 'partialAPKHash',
|
||||||
]
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
excludeCommonSettingKeys = [
|
excludeCommonSettingKeys = [
|
||||||
'versionExtractionRegEx',
|
'versionExtractionRegEx',
|
||||||
@ -32,7 +35,7 @@ class DirectAPKLink extends AppSource {
|
|||||||
'versionDetection',
|
'versionDetection',
|
||||||
'useVersionCodeAsOSVersion',
|
'useVersionCodeAsOSVersion',
|
||||||
'apkFilterRegEx',
|
'apkFilterRegEx',
|
||||||
'autoApkFilterByArch'
|
'autoApkFilterByArch',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,10 +54,13 @@ class DirectAPKLink extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) {
|
bool forAPKDownload = false,
|
||||||
return html.getRequestHeaders(additionalSettings,
|
}) {
|
||||||
forAPKDownload: forAPKDownload);
|
return html.getRequestHeaders(
|
||||||
|
additionalSettings,
|
||||||
|
forAPKDownload: forAPKDownload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -62,8 +68,9 @@ class DirectAPKLink extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var additionalSettingsNew =
|
var additionalSettingsNew = getDefaultValuesFromFormItems(
|
||||||
getDefaultValuesFromFormItems(html.combinedAppSpecificSettingFormItems);
|
html.combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
for (var s in additionalSettings.keys) {
|
for (var s in additionalSettings.keys) {
|
||||||
if (additionalSettingsNew.containsKey(s)) {
|
if (additionalSettingsNew.containsKey(s)) {
|
||||||
additionalSettingsNew[s] = additionalSettings[s];
|
additionalSettingsNew[s] = additionalSettings[s];
|
||||||
|
@ -17,22 +17,28 @@ class FDroid extends AppSource {
|
|||||||
canSearch = true;
|
canSearch = true;
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterVersionsByRegEx',
|
GeneratedFormTextField(
|
||||||
|
'filterVersionsByRegEx',
|
||||||
label: tr('filterVersionsByRegEx'),
|
label: tr('filterVersionsByRegEx'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('trySelectingSuggestedVersionCode'))
|
'trySelectingSuggestedVersionCode',
|
||||||
|
label: tr('trySelectingSuggestedVersionCode'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('autoSelectHighestVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('autoSelectHighestVersionCode'))
|
'autoSelectHighestVersionCode',
|
||||||
|
label: tr('autoSelectHighestVersionCode'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -41,7 +47,8 @@ class FDroid extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+[^/]+/+packages/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
@ -49,7 +56,8 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/+packages/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -58,8 +66,10 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,21 +82,27 @@ class FDroid extends AppSource {
|
|||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
var details = getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'https://$host/api/v1/packages/$appId', additionalSettings),
|
'https://$host/api/v1/packages/$appId',
|
||||||
|
additionalSettings,
|
||||||
|
),
|
||||||
'https://$host/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl,
|
standardUrl,
|
||||||
name,
|
name,
|
||||||
additionalSettings: additionalSettings);
|
additionalSettings: additionalSettings,
|
||||||
|
);
|
||||||
if (!hostChanged) {
|
if (!hostChanged) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
var lines = res.body.split('\n');
|
var lines = res.body.split('\n');
|
||||||
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
|
||||||
if (authorLines.isNotEmpty) {
|
if (authorLines.isNotEmpty) {
|
||||||
details.names.author =
|
details.names.author = authorLines.first
|
||||||
authorLines.first.split(': ').sublist(1).join(': ');
|
.split(': ')
|
||||||
|
.sublist(1)
|
||||||
|
.join(': ');
|
||||||
}
|
}
|
||||||
var changelogUrls = lines
|
var changelogUrls = lines
|
||||||
.where((l) => l.startsWith('Changelog: '))
|
.where((l) => l.startsWith('Changelog: '))
|
||||||
@ -111,8 +127,8 @@ class FDroid extends AppSource {
|
|||||||
(details.changeLog?.indexOf('/blob/') ?? -1) >= 0) {
|
(details.changeLog?.indexOf('/blob/') ?? -1) >= 0) {
|
||||||
details.changeLog = (await sourceRequest(
|
details.changeLog = (await sourceRequest(
|
||||||
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
details.changeLog!.replaceFirst('/blob/', '/raw/'),
|
||||||
additionalSettings))
|
additionalSettings,
|
||||||
.body;
|
)).body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -126,10 +142,14 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
|
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}',
|
||||||
|
{},
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
@ -145,7 +165,7 @@ class FDroid extends AppSource {
|
|||||||
urlsWithDescriptions[url] = [
|
urlsWithDescriptions[url] = [
|
||||||
e.querySelector('.package-name')?.text.trim() ?? '',
|
e.querySelector('.package-name')?.text.trim() ?? '',
|
||||||
e.querySelector('.package-summary')?.text.trim() ??
|
e.querySelector('.package-summary')?.text.trim() ??
|
||||||
tr('noDescription')
|
tr('noDescription'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -156,8 +176,12 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
Response res, String apkUrlPrefix, String standardUrl, String sourceName,
|
Response res,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
String apkUrlPrefix,
|
||||||
|
String standardUrl,
|
||||||
|
String sourceName, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) {
|
||||||
var autoSelectHighestVersionCode =
|
var autoSelectHighestVersionCode =
|
||||||
additionalSettings['autoSelectHighestVersionCode'] == true;
|
additionalSettings['autoSelectHighestVersionCode'] == true;
|
||||||
var trySelectingSuggestedVersionCode =
|
var trySelectingSuggestedVersionCode =
|
||||||
@ -177,8 +201,11 @@ class FDroid extends AppSource {
|
|||||||
if (apkFilterRegEx != null) {
|
if (apkFilterRegEx != null) {
|
||||||
releases = releases.where((rel) {
|
releases = releases.where((rel) {
|
||||||
String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk';
|
String apk = '${apkUrlPrefix}_${rel['versionCode']}.apk';
|
||||||
return filterApks([MapEntry(apk, apk)], apkFilterRegEx, false)
|
return filterApks(
|
||||||
.isNotEmpty;
|
[MapEntry(apk, apk)],
|
||||||
|
apkFilterRegEx,
|
||||||
|
false,
|
||||||
|
).isNotEmpty;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
if (releases.isEmpty) {
|
if (releases.isEmpty) {
|
||||||
@ -191,8 +218,10 @@ class FDroid extends AppSource {
|
|||||||
if (trySelectingSuggestedVersionCode &&
|
if (trySelectingSuggestedVersionCode &&
|
||||||
response['suggestedVersionCode'] != null &&
|
response['suggestedVersionCode'] != null &&
|
||||||
filterVersionsByRegEx == null) {
|
filterVersionsByRegEx == null) {
|
||||||
var suggestedReleases = releases.where((element) =>
|
var suggestedReleases = releases.where(
|
||||||
element['versionCode'] == response['suggestedVersionCode']);
|
(element) =>
|
||||||
|
element['versionCode'] == response['suggestedVersionCode'],
|
||||||
|
);
|
||||||
if (suggestedReleases.isNotEmpty) {
|
if (suggestedReleases.isNotEmpty) {
|
||||||
releaseChoices = suggestedReleases;
|
releaseChoices = suggestedReleases;
|
||||||
version = suggestedReleases.first['versionName'];
|
version = suggestedReleases.first['versionName'];
|
||||||
@ -203,8 +232,9 @@ class FDroid extends AppSource {
|
|||||||
version = null;
|
version = null;
|
||||||
releaseChoices = [];
|
releaseChoices = [];
|
||||||
for (var i = 0; i < releases.length; i++) {
|
for (var i = 0; i < releases.length; i++) {
|
||||||
if (RegExp(filterVersionsByRegEx!)
|
if (RegExp(
|
||||||
.hasMatch(releases[i]['versionName'])) {
|
filterVersionsByRegEx!,
|
||||||
|
).hasMatch(releases[i]['versionName'])) {
|
||||||
version = 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 a suggested release was not already picked, pick all those with the selected version
|
||||||
if (releaseChoices.isEmpty) {
|
if (releaseChoices.isEmpty) {
|
||||||
releaseChoices =
|
releaseChoices = releases.where(
|
||||||
releases.where((element) => element['versionName'] == version);
|
(element) => element['versionName'] == version,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// For the remaining releases, use the toggles to auto-select one if possible
|
// For the remaining releases, use the toggles to auto-select one if possible
|
||||||
if (releaseChoices.length > 1) {
|
if (releaseChoices.length > 1) {
|
||||||
@ -228,8 +259,10 @@ class FDroid extends AppSource {
|
|||||||
releaseChoices = [releaseChoices.first];
|
releaseChoices = [releaseChoices.first];
|
||||||
} else if (trySelectingSuggestedVersionCode &&
|
} else if (trySelectingSuggestedVersionCode &&
|
||||||
response['suggestedVersionCode'] != null) {
|
response['suggestedVersionCode'] != null) {
|
||||||
var suggestedReleases = releaseChoices.where((element) =>
|
var suggestedReleases = releaseChoices.where(
|
||||||
element['versionCode'] == response['suggestedVersionCode']);
|
(element) =>
|
||||||
|
element['versionCode'] == response['suggestedVersionCode'],
|
||||||
|
);
|
||||||
if (suggestedReleases.isNotEmpty) {
|
if (suggestedReleases.isNotEmpty) {
|
||||||
releaseChoices = suggestedReleases;
|
releaseChoices = suggestedReleases;
|
||||||
}
|
}
|
||||||
@ -241,8 +274,11 @@ class FDroid extends AppSource {
|
|||||||
List<String> apkUrls = releaseChoices
|
List<String> apkUrls = releaseChoices
|
||||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
return APKDetails(
|
||||||
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
|
version,
|
||||||
|
getApkUrlsFromUrls(apkUrls.toSet().toList()),
|
||||||
|
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,20 @@ class FDroidRepo extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appIdOrName',
|
GeneratedFormTextField(
|
||||||
|
'appIdOrName',
|
||||||
label: tr('appIdOrName'),
|
label: tr('appIdOrName'),
|
||||||
hint: tr('reposHaveMultipleApps'),
|
hint: tr('reposHaveMultipleApps'),
|
||||||
required: true)
|
required: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('pickHighestVersionCode',
|
GeneratedFormSwitch(
|
||||||
label: tr('pickHighestVersionCode'), defaultValue: false)
|
'pickHighestVersionCode',
|
||||||
]
|
label: tr('pickHighestVersionCode'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,8 +59,10 @@ class FDroidRepo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
String? url = querySettings['url'];
|
String? url = querySettings['url'];
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
@ -73,11 +80,8 @@ class FDroidRepo extends AppSource {
|
|||||||
appId.contains(query) ||
|
appId.contains(query) ||
|
||||||
appName.contains(query) ||
|
appName.contains(query) ||
|
||||||
appDesc.contains(query)) {
|
appDesc.contains(query)) {
|
||||||
results[
|
results['${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] =
|
||||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
|
[appName, appDesc];
|
||||||
appName,
|
|
||||||
appDesc
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
@ -119,8 +123,11 @@ class FDroidRepo extends AppSource {
|
|||||||
if (appId != null) {
|
if (appId != null) {
|
||||||
app.url = uri
|
app.url = uri
|
||||||
.replace(
|
.replace(
|
||||||
queryParameters: Map.fromEntries(
|
queryParameters: Map.fromEntries([
|
||||||
[...uri.queryParameters.entries, MapEntry('appId', appId)]))
|
...uri.queryParameters.entries,
|
||||||
|
MapEntry('appId', appId),
|
||||||
|
]),
|
||||||
|
)
|
||||||
.toString();
|
.toString();
|
||||||
app.additionalSettings['appIdOrName'] = appId;
|
app.additionalSettings['appIdOrName'] = appId;
|
||||||
app.id = appId;
|
app.id = appId;
|
||||||
@ -134,7 +141,8 @@ class FDroidRepo extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var base = url.endsWith('/index.xml')
|
var base = url.endsWith('/index.xml')
|
||||||
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
? url.split('/').reversed.toList().sublist(1).reversed.join('/')
|
||||||
@ -142,7 +150,9 @@ class FDroidRepo extends AppSource {
|
|||||||
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
res = await sourceRequest('$base/repo/index.xml', additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
res = await sourceRequest(
|
res = await sourceRequest(
|
||||||
'$base/fdroid/repo/index.xml', additionalSettings);
|
'$base/fdroid/repo/index.xml',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
@ -164,8 +174,10 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
additionalSettings['appIdOrName'] = appIdOrName;
|
additionalSettings['appIdOrName'] = appIdOrName;
|
||||||
var res =
|
var res = await sourceRequestWithURLVariants(
|
||||||
await sourceRequestWithURLVariants(standardUrl, additionalSettings);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var body = parse(res.body);
|
var body = parse(res.body);
|
||||||
var foundApps = body.querySelectorAll('application').where((element) {
|
var foundApps = body.querySelectorAll('application').where((element) {
|
||||||
@ -202,24 +214,32 @@ class FDroidRepo extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
var latestVersionReleases = releases
|
var latestVersionReleases = releases
|
||||||
.where((element) =>
|
.where(
|
||||||
|
(element) =>
|
||||||
element.querySelector('version')?.innerHtml == latestVersion &&
|
element.querySelector('version')?.innerHtml == latestVersion &&
|
||||||
element.querySelector('apkname') != null)
|
element.querySelector('apkname') != null,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (latestVersionReleases.length > 1 && pickHighestVersionCode) {
|
if (latestVersionReleases.length > 1 && pickHighestVersionCode) {
|
||||||
latestVersionReleases.sort((e1, e2) {
|
latestVersionReleases.sort((e1, e2) {
|
||||||
return int.parse(e2.querySelector('versioncode')!.innerHtml)
|
return int.parse(
|
||||||
.compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml));
|
e2.querySelector('versioncode')!.innerHtml,
|
||||||
|
).compareTo(int.parse(e1.querySelector('versioncode')!.innerHtml));
|
||||||
});
|
});
|
||||||
latestVersionReleases = [latestVersionReleases[0]];
|
latestVersionReleases = [latestVersionReleases[0]];
|
||||||
}
|
}
|
||||||
List<String> apkUrls = latestVersionReleases
|
List<String> apkUrls = latestVersionReleases
|
||||||
.map((e) =>
|
.map(
|
||||||
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
|
(e) =>
|
||||||
|
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}',
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
return APKDetails(
|
||||||
|
latestVersion,
|
||||||
|
getApkUrlsFromUrls(apkUrls),
|
||||||
AppNames(authorName, appName),
|
AppNames(authorName, appName),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -19,59 +19,71 @@ class GitHub extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField(
|
||||||
|
'github-creds',
|
||||||
label: tr('githubPATLabel'),
|
label: tr('githubPATLabel'),
|
||||||
password: true,
|
password: true,
|
||||||
required: false,
|
required: false,
|
||||||
belowWidgets: [
|
belowWidgets: [
|
||||||
const SizedBox(
|
const SizedBox(height: 4),
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('about'),
|
tr('about'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
decoration: TextDecoration.underline,
|
||||||
)),
|
fontSize: 12,
|
||||||
const SizedBox(
|
),
|
||||||
height: 4,
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
])
|
|
||||||
];
|
];
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('includePrereleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('includePrereleases'), defaultValue: false)
|
'includePrereleases',
|
||||||
|
label: tr('includePrereleases'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
GeneratedFormTextField(
|
||||||
|
'filterReleaseTitlesByRegEx',
|
||||||
label: tr('filterReleaseTitlesByRegEx'),
|
label: tr('filterReleaseTitlesByRegEx'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filterReleaseNotesByRegEx',
|
GeneratedFormTextField(
|
||||||
|
'filterReleaseNotesByRegEx',
|
||||||
label: tr('filterReleaseNotesByRegEx'),
|
label: tr('filterReleaseNotesByRegEx'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
|
[GeneratedFormSwitch('verifyLatestTag', label: tr('verifyLatestTag'))],
|
||||||
[
|
[
|
||||||
@ -81,26 +93,36 @@ class GitHub extends AppSource {
|
|||||||
MapEntry('date', tr('releaseDate')),
|
MapEntry('date', tr('releaseDate')),
|
||||||
MapEntry('smartname', tr('smartname')),
|
MapEntry('smartname', tr('smartname')),
|
||||||
MapEntry('none', tr('none')),
|
MapEntry('none', tr('none')),
|
||||||
MapEntry('smartname-datefallback',
|
MapEntry(
|
||||||
'${tr('smartname')} x ${tr('releaseDate')}'),
|
'smartname-datefallback',
|
||||||
|
'${tr('smartname')} x ${tr('releaseDate')}',
|
||||||
|
),
|
||||||
MapEntry('name', tr('name')),
|
MapEntry('name', tr('name')),
|
||||||
],
|
],
|
||||||
label: tr('sortMethod'),
|
label: tr('sortMethod'),
|
||||||
defaultValue: 'date')
|
defaultValue: 'date',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('useLatestAssetDateAsReleaseDate',
|
GeneratedFormSwitch(
|
||||||
label: tr('useLatestAssetDateAsReleaseDate'), defaultValue: false)
|
'useLatestAssetDateAsReleaseDate',
|
||||||
|
label: tr('useLatestAssetDateAsReleaseDate'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('releaseTitleAsVersion',
|
GeneratedFormSwitch(
|
||||||
label: tr('releaseTitleAsVersion'), defaultValue: false)
|
'releaseTitleAsVersion',
|
||||||
]
|
label: tr('releaseTitleAsVersion'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
canSearch = true;
|
canSearch = true;
|
||||||
searchQuerySettingFormItems = [
|
searchQuerySettingFormItems = [
|
||||||
GeneratedFormTextField('minStarCount',
|
GeneratedFormTextField(
|
||||||
|
'minStarCount',
|
||||||
label: tr('minStarCount'),
|
label: tr('minStarCount'),
|
||||||
defaultValue: '0',
|
defaultValue: '0',
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
@ -111,53 +133,71 @@ class GitHub extends AppSource {
|
|||||||
return tr('invalidInput');
|
return tr('invalidInput');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
const possibleBuildGradleLocations = [
|
const possibleBuildGradleLocations = [
|
||||||
'/app/build.gradle',
|
'/app/build.gradle',
|
||||||
'android/app/build.gradle',
|
'android/app/build.gradle',
|
||||||
'src/app/build.gradle'
|
'src/app/build.gradle',
|
||||||
];
|
];
|
||||||
for (var path in possibleBuildGradleLocations) {
|
for (var path in possibleBuildGradleLocations) {
|
||||||
try {
|
try {
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
try {
|
try {
|
||||||
var body = jsonDecode(res.body);
|
var body = jsonDecode(res.body);
|
||||||
var trimmedLines = utf8
|
var trimmedLines = utf8
|
||||||
.decode(base64
|
.decode(
|
||||||
.decode(body['content'].toString().split('\n').join('')))
|
base64.decode(
|
||||||
|
body['content'].toString().split('\n').join(''),
|
||||||
|
),
|
||||||
|
)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((e) => e.trim());
|
.map((e) => e.trim());
|
||||||
var appIds = trimmedLines.where((l) =>
|
var appIds = trimmedLines.where(
|
||||||
|
(l) =>
|
||||||
l.startsWith('applicationId "') ||
|
l.startsWith('applicationId "') ||
|
||||||
l.startsWith('applicationId \''));
|
l.startsWith('applicationId \''),
|
||||||
appIds = appIds.map((appId) => appId
|
);
|
||||||
.split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
|
appIds = appIds.map(
|
||||||
appIds = appIds.map((appId) {
|
(appId) => appId.split(
|
||||||
|
appId.startsWith('applicationId "') ? '"' : '\'',
|
||||||
|
)[1],
|
||||||
|
);
|
||||||
|
appIds = appIds
|
||||||
|
.map((appId) {
|
||||||
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
||||||
appId = trimmedLines
|
appId = trimmedLines
|
||||||
.where((l) => l.startsWith(
|
.where(
|
||||||
'def ${appId.substring(2, appId.length - 1)}'))
|
(l) => l.startsWith(
|
||||||
|
'def ${appId.substring(2, appId.length - 1)}',
|
||||||
|
),
|
||||||
|
)
|
||||||
.first;
|
.first;
|
||||||
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
||||||
}
|
}
|
||||||
return appId;
|
return appId;
|
||||||
}).where((appId) => appId.isNotEmpty);
|
})
|
||||||
|
.where((appId) => appId.isNotEmpty);
|
||||||
if (appIds.length == 1) {
|
if (appIds.length == 1) {
|
||||||
return appIds.first;
|
return appIds.first;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LogsProvider().add(
|
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) {
|
} catch (err) {
|
||||||
@ -171,7 +211,8 @@ class GitHub extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -181,8 +222,9 @@ class GitHub extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
var token = await getTokenIfAny(additionalSettings);
|
var token = await getTokenIfAny(additionalSettings);
|
||||||
var headers = <String, String>{};
|
var headers = <String, String>{};
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
@ -201,14 +243,17 @@ class GitHub extends AppSource {
|
|||||||
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
var sourceConfig =
|
var sourceConfig = await getSourceConfigValues(
|
||||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
additionalSettings,
|
||||||
|
settingsProvider,
|
||||||
|
);
|
||||||
String? creds = sourceConfig['github-creds'];
|
String? creds = sourceConfig['github-creds'];
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
var userNameEndIndex = creds.indexOf(':');
|
var userNameEndIndex = creds.indexOf(':');
|
||||||
if (userNameEndIndex > 0) {
|
if (userNameEndIndex > 0) {
|
||||||
creds = creds.substring(
|
creds = creds.substring(
|
||||||
userNameEndIndex + 1); // For old username-included token inputs
|
userNameEndIndex + 1,
|
||||||
|
); // For old username-included token inputs
|
||||||
}
|
}
|
||||||
return creds;
|
return creds;
|
||||||
} else {
|
} else {
|
||||||
@ -228,16 +273,21 @@ class GitHub extends AppSource {
|
|||||||
'https://api.${hosts[0]}';
|
'https://api.${hosts[0]}';
|
||||||
|
|
||||||
Future<String> convertStandardUrlToAPIUrl(
|
Future<String> convertStandardUrlToAPIUrl(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async =>
|
||||||
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
|
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://${hosts[0]}'.length)}';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/releases';
|
'$standardUrl/releases';
|
||||||
|
|
||||||
Future<APKDetails> getLatestAPKDetailsCommon(String requestUrl,
|
Future<APKDetails> getLatestAPKDetailsCommon(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings,
|
String requestUrl,
|
||||||
{Function(Response)? onHttpErrorCode}) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings, {
|
||||||
|
Function(Response)? onHttpErrorCode,
|
||||||
|
}) async {
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
@ -263,7 +313,8 @@ class GitHub extends AppSource {
|
|||||||
var temp = requestUrl.split('?');
|
var temp = requestUrl.split('?');
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
|
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
if (onHttpErrorCode != null) {
|
if (onHttpErrorCode != null) {
|
||||||
onHttpErrorCode(res);
|
onHttpErrorCode(res);
|
||||||
@ -278,8 +329,10 @@ class GitHub extends AppSource {
|
|||||||
if (latestRelease != null) {
|
if (latestRelease != null) {
|
||||||
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
|
||||||
if (releases
|
if (releases
|
||||||
.where((element) =>
|
.where(
|
||||||
(element['tag_name'] ?? element['name']) == latestTag)
|
(element) =>
|
||||||
|
(element['tag_name'] ?? element['name']) == latestTag,
|
||||||
|
)
|
||||||
.isEmpty) {
|
.isEmpty) {
|
||||||
releases = [latestRelease, ...releases];
|
releases = [latestRelease, ...releases];
|
||||||
}
|
}
|
||||||
@ -340,17 +393,25 @@ class GitHub extends AppSource {
|
|||||||
} else {
|
} else {
|
||||||
var nameA = a['tag_name'] ?? a['name'];
|
var nameA = a['tag_name'] ?? a['name'];
|
||||||
var nameB = b['tag_name'] ?? b['name'];
|
var nameB = b['tag_name'] ?? b['name'];
|
||||||
var stdFormats = findStandardFormatsForVersion(nameA, false)
|
var stdFormats = findStandardFormatsForVersion(
|
||||||
.intersection(findStandardFormatsForVersion(nameB, false));
|
nameA,
|
||||||
|
false,
|
||||||
|
).intersection(findStandardFormatsForVersion(nameB, false));
|
||||||
if (sortMethod == 'date' ||
|
if (sortMethod == 'date' ||
|
||||||
(sortMethod == 'smartname-datefallback' &&
|
(sortMethod == 'smartname-datefallback' &&
|
||||||
stdFormats.isEmpty)) {
|
stdFormats.isEmpty)) {
|
||||||
return (getReleaseDateFromRelease(
|
return (getReleaseDateFromRelease(
|
||||||
a, useLatestAssetDateAsReleaseDate) ??
|
a,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
) ??
|
||||||
DateTime(1))
|
DateTime(1))
|
||||||
.compareTo(getReleaseDateFromRelease(
|
.compareTo(
|
||||||
b, useLatestAssetDateAsReleaseDate) ??
|
getReleaseDateFromRelease(
|
||||||
DateTime(0));
|
b,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
) ??
|
||||||
|
DateTime(0),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (sortMethod != 'name' && stdFormats.isNotEmpty) {
|
if (sortMethod != 'name' && stdFormats.isNotEmpty) {
|
||||||
var reg = RegExp(stdFormats.last);
|
var reg = RegExp(stdFormats.last);
|
||||||
@ -358,11 +419,14 @@ class GitHub extends AppSource {
|
|||||||
var matchB = reg.firstMatch(nameB);
|
var matchB = reg.firstMatch(nameB);
|
||||||
return compareAlphaNumeric(
|
return compareAlphaNumeric(
|
||||||
(nameA as String).substring(matchA!.start, matchA.end),
|
(nameA as String).substring(matchA!.start, matchA.end),
|
||||||
(nameB as String).substring(matchB!.start, matchB.end));
|
(nameB as String).substring(matchB!.start, matchB.end),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 'name'
|
// 'name'
|
||||||
return compareAlphaNumeric(
|
return compareAlphaNumeric(
|
||||||
(nameA as String), (nameB as String));
|
(nameA as String),
|
||||||
|
(nameB as String),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -374,9 +438,11 @@ class GitHub extends AppSource {
|
|||||||
latestRelease !=
|
latestRelease !=
|
||||||
(releases[releases.length - 1]['tag_name'] ??
|
(releases[releases.length - 1]['tag_name'] ??
|
||||||
releases[0]['name'])) {
|
releases[0]['name'])) {
|
||||||
var ind = releases.indexWhere((element) =>
|
var ind = releases.indexWhere(
|
||||||
|
(element) =>
|
||||||
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
(latestRelease['tag_name'] ?? latestRelease['name']) ==
|
||||||
(element['tag_name'] ?? element['name']));
|
(element['tag_name'] ?? element['name']),
|
||||||
|
);
|
||||||
if (ind >= 0) {
|
if (ind >= 0) {
|
||||||
releases.add(releases.removeAt(ind));
|
releases.add(releases.removeAt(ind));
|
||||||
}
|
}
|
||||||
@ -404,8 +470,9 @@ class GitHub extends AppSource {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (regexNotesFilter != null &&
|
if (regexNotesFilter != null &&
|
||||||
!RegExp(regexNotesFilter)
|
!RegExp(
|
||||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
regexNotesFilter,
|
||||||
|
).hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var allAssetsWithUrls = findReleaseAssetUrls(releases[i]);
|
var allAssetsWithUrls = findReleaseAssetUrls(releases[i]);
|
||||||
@ -413,11 +480,12 @@ class GitHub extends AppSource {
|
|||||||
.map((e) => e['final_url'] as MapEntry<String, String>)
|
.map((e) => e['final_url'] as MapEntry<String, String>)
|
||||||
.toList();
|
.toList();
|
||||||
var apkAssetsWithUrls = allAssetsWithUrls
|
var apkAssetsWithUrls = allAssetsWithUrls
|
||||||
.where((element) =>
|
.where(
|
||||||
(element['final_url'] as MapEntry<String, String>)
|
(element) => (element['final_url'] as MapEntry<String, String>)
|
||||||
.key
|
.key
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith('.apk'))
|
.endsWith('.apk'),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
var filteredApkUrls = filterApks(
|
var filteredApkUrls = filterApks(
|
||||||
@ -425,12 +493,18 @@ class GitHub extends AppSource {
|
|||||||
.map((e) => e['final_url'] as MapEntry<String, String>)
|
.map((e) => e['final_url'] as MapEntry<String, String>)
|
||||||
.toList(),
|
.toList(),
|
||||||
additionalSettings['apkFilterRegEx'],
|
additionalSettings['apkFilterRegEx'],
|
||||||
additionalSettings['invertAPKFilter']);
|
additionalSettings['invertAPKFilter'],
|
||||||
|
);
|
||||||
var filteredApks = apkAssetsWithUrls
|
var filteredApks = apkAssetsWithUrls
|
||||||
.where((e) => filteredApkUrls
|
.where(
|
||||||
.where((e2) =>
|
(e) => filteredApkUrls
|
||||||
e2.key == (e['final_url'] as MapEntry<String, String>).key)
|
.where(
|
||||||
.isNotEmpty)
|
(e2) =>
|
||||||
|
e2.key ==
|
||||||
|
(e['final_url'] as MapEntry<String, String>).key,
|
||||||
|
)
|
||||||
|
.isNotEmpty,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (filteredApks.isEmpty && additionalSettings['trackOnly'] != true) {
|
if (filteredApks.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||||
@ -444,14 +518,20 @@ class GitHub extends AppSource {
|
|||||||
? nameToFilter
|
? nameToFilter
|
||||||
: targetRelease['tag_name'] ?? targetRelease['name'];
|
: targetRelease['tag_name'] ?? targetRelease['name'];
|
||||||
if (targetRelease['tarball_url'] != null) {
|
if (targetRelease['tarball_url'] != null) {
|
||||||
allAssetUrls.add(MapEntry(
|
allAssetUrls.add(
|
||||||
|
MapEntry(
|
||||||
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
||||||
targetRelease['tarball_url']));
|
targetRelease['tarball_url'],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (targetRelease['zipball_url'] != null) {
|
if (targetRelease['zipball_url'] != null) {
|
||||||
allAssetUrls.add(MapEntry(
|
allAssetUrls.add(
|
||||||
|
MapEntry(
|
||||||
(targetRelease['version'] ?? 'source') + '.zip',
|
(targetRelease['version'] ?? 'source') + '.zip',
|
||||||
targetRelease['zipball_url']));
|
targetRelease['zipball_url'],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
targetRelease['allAssetUrls'] = allAssetUrls;
|
targetRelease['allAssetUrls'] = allAssetUrls;
|
||||||
break;
|
break;
|
||||||
@ -462,7 +542,9 @@ class GitHub extends AppSource {
|
|||||||
String? version = targetRelease['version'];
|
String? version = targetRelease['version'];
|
||||||
|
|
||||||
DateTime? releaseDate = getReleaseDateFromRelease(
|
DateTime? releaseDate = getReleaseDateFromRelease(
|
||||||
targetRelease, useLatestAssetDateAsReleaseDate);
|
targetRelease,
|
||||||
|
useLatestAssetDateAsReleaseDate,
|
||||||
|
);
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -474,7 +556,8 @@ class GitHub extends AppSource {
|
|||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
changeLog: changeLog.isEmpty ? null : changeLog,
|
changeLog: changeLog.isEmpty ? null : changeLog,
|
||||||
allAssetUrls:
|
allAssetUrls:
|
||||||
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
|
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (onHttpErrorCode != null) {
|
if (onHttpErrorCode != null) {
|
||||||
onHttpErrorCode(res);
|
onHttpErrorCode(res);
|
||||||
@ -483,20 +566,27 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestAPKDetailsCommon2(
|
Future<APKDetails> getLatestAPKDetailsCommon2(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
Future<String> Function(bool) reqUrlGenerator,
|
Future<String> Function(bool) reqUrlGenerator,
|
||||||
dynamic Function(Response)? onHttpErrorCode) async {
|
dynamic Function(Response)? onHttpErrorCode,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
return await getLatestAPKDetailsCommon(
|
return await getLatestAPKDetailsCommon(
|
||||||
await reqUrlGenerator(false), standardUrl, additionalSettings,
|
await reqUrlGenerator(false),
|
||||||
onHttpErrorCode: onHttpErrorCode);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
||||||
return await getLatestAPKDetailsCommon(
|
return await getLatestAPKDetailsCommon(
|
||||||
await reqUrlGenerator(true), standardUrl, additionalSettings,
|
await reqUrlGenerator(true),
|
||||||
onHttpErrorCode: onHttpErrorCode);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@ -508,12 +598,16 @@ class GitHub extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
return await getLatestAPKDetailsCommon2(
|
||||||
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
(bool useTagUrl) async {
|
(bool useTagUrl) async {
|
||||||
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
}, (Response res) {
|
},
|
||||||
|
(Response res) {
|
||||||
rateLimitErrorCheck(res);
|
rateLimitErrorCheck(res);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
@ -523,9 +617,12 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<String>>> searchCommon(
|
Future<Map<String, List<String>>> searchCommon(
|
||||||
String query, String requestUrl, String rootProp,
|
String query,
|
||||||
{Function(Response)? onHttpErrorCode,
|
String requestUrl,
|
||||||
Map<String, dynamic> querySettings = const {}}) async {
|
String rootProp, {
|
||||||
|
Function(Response)? onHttpErrorCode,
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
Response res = await sourceRequest(requestUrl, {});
|
Response res = await sourceRequest(requestUrl, {});
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
int minStarCount = querySettings['minStarCount'] != null
|
int minStarCount = querySettings['minStarCount'] != null
|
||||||
@ -540,8 +637,8 @@ class GitHub extends AppSource {
|
|||||||
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
||||||
(e['description'] != null
|
(e['description'] != null
|
||||||
? e['description'] as String
|
? e['description'] as String
|
||||||
: tr('noDescription')))
|
: tr('noDescription'))),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -555,22 +652,27 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
return searchCommon(
|
return searchCommon(
|
||||||
query,
|
query,
|
||||||
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||||
'items', onHttpErrorCode: (Response res) {
|
'items',
|
||||||
|
onHttpErrorCode: (Response res) {
|
||||||
rateLimitErrorCheck(res);
|
rateLimitErrorCheck(res);
|
||||||
}, querySettings: querySettings);
|
},
|
||||||
|
querySettings: querySettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimitErrorCheck(Response res) {
|
void rateLimitErrorCheck(Response res) {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
throw RateLimitError(
|
throw RateLimitError(
|
||||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000)
|
||||||
60000000)
|
.round(),
|
||||||
.round());
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,36 +18,41 @@ class GitLab extends AppSource {
|
|||||||
showReleaseDateAsVersionToggle = true;
|
showReleaseDateAsVersionToggle = true;
|
||||||
|
|
||||||
sourceConfigSettingFormItems = [
|
sourceConfigSettingFormItems = [
|
||||||
GeneratedFormTextField('gitlab-creds',
|
GeneratedFormTextField(
|
||||||
|
'gitlab-creds',
|
||||||
label: tr('gitlabPATLabel'),
|
label: tr('gitlabPATLabel'),
|
||||||
password: true,
|
password: true,
|
||||||
required: false,
|
required: false,
|
||||||
belowWidgets: [
|
belowWidgets: [
|
||||||
const SizedBox(
|
const SizedBox(height: 4),
|
||||||
height: 4,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
|
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
|
||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('about'),
|
tr('about'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
decoration: TextDecoration.underline,
|
||||||
)),
|
fontSize: 12,
|
||||||
const SizedBox(
|
),
|
||||||
height: 4,
|
),
|
||||||
)
|
),
|
||||||
])
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
]
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,11 +60,13 @@ class GitLab extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
var urlSegments = url.split('/');
|
var urlSegments = url.split('/');
|
||||||
var cutOffIndex = urlSegments.indexWhere((s) => s == '-');
|
var cutOffIndex = urlSegments.indexWhere((s) => s == '-');
|
||||||
url =
|
url = urlSegments
|
||||||
urlSegments.sublist(0, cutOffIndex <= 0 ? null : cutOffIndex).join('/');
|
.sublist(0, cutOffIndex <= 0 ? null : cutOffIndex)
|
||||||
|
.join('/');
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+(/[^((\b/\b)|(\b/-/\b))]+){1,20}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -70,15 +77,19 @@ class GitLab extends AppSource {
|
|||||||
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
var sourceConfig =
|
var sourceConfig = await getSourceConfigValues(
|
||||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
additionalSettings,
|
||||||
|
settingsProvider,
|
||||||
|
);
|
||||||
String? creds = sourceConfig['gitlab-creds'];
|
String? creds = sourceConfig['gitlab-creds'];
|
||||||
return creds != null && creds.isNotEmpty ? creds : null;
|
return creds != null && creds.isNotEmpty ? creds : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
var url =
|
var url =
|
||||||
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
'https://${hosts[0]}/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
|
||||||
var res = await sourceRequest(url, {});
|
var res = await sourceRequest(url, {});
|
||||||
@ -90,7 +101,7 @@ class GitLab extends AppSource {
|
|||||||
for (var element in json) {
|
for (var element in json) {
|
||||||
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
|
||||||
element['name_with_namespace'],
|
element['name_with_namespace'],
|
||||||
element['description'] ?? tr('noDescription')
|
element['description'] ?? tr('noDescription'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@ -102,8 +113,9 @@ class GitLab extends AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
// Change headers to pacify, e.g. cloudflare protection
|
// Change headers to pacify, e.g. cloudflare protection
|
||||||
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
// Related to: (#1397, #1389, #1384, #1382, #1381, #1380, #1359, #854, #785, #697)
|
||||||
var headers = <String, String>{};
|
var headers = <String, String>{};
|
||||||
@ -116,8 +128,11 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||||
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
String optionalAuth = (PAT != null) ? 'private_token=$PAT' : '';
|
||||||
return '$apkUrl${(Uri.parse(apkUrl).query.isEmpty ? '?' : '&')}$optionalAuth';
|
return '$apkUrl${(Uri.parse(apkUrl).query.isEmpty ? '?' : '&')}$optionalAuth';
|
||||||
@ -140,7 +155,8 @@ class GitLab extends AppSource {
|
|||||||
// Get project ID
|
// Get project ID
|
||||||
Response res0 = await sourceRequest(
|
Response res0 = await sourceRequest(
|
||||||
'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth',
|
'https://${hosts[0]}/api/v4/projects/$projectUriComponent?$optionalAuth',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res0.statusCode != 200) {
|
if (res0.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res0);
|
throw getObtainiumHttpError(res0);
|
||||||
}
|
}
|
||||||
@ -152,7 +168,8 @@ class GitLab extends AppSource {
|
|||||||
// Request data from REST API
|
// Request data from REST API
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
|
'https://${hosts[0]}/api/v4/projects/$projectUriComponent/${trackOnly ? 'repository/tags' : 'releases'}?$optionalAuth',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -169,8 +186,10 @@ class GitLab extends AppSource {
|
|||||||
(e['name'] ??
|
(e['name'] ??
|
||||||
(parsedUrl != null && parsedUrl.pathSegments.isNotEmpty
|
(parsedUrl != null && parsedUrl.pathSegments.isNotEmpty
|
||||||
? parsedUrl.pathSegments.last
|
? parsedUrl.pathSegments.last
|
||||||
: 'unknown')) as String,
|
: 'unknown'))
|
||||||
(e['direct_asset_url'] ?? e['url'] ?? '') as String);
|
as String,
|
||||||
|
(e['direct_asset_url'] ?? e['url'] ?? '') as String,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.where((s) => s.key.isNotEmpty)
|
.where((s) => s.key.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
@ -193,11 +212,15 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
var releaseDateString =
|
var releaseDateString =
|
||||||
e['released_at'] ?? e['created_at'] ?? e['commit']?['created_at'];
|
e['released_at'] ?? e['created_at'] ?? e['commit']?['created_at'];
|
||||||
DateTime? releaseDate =
|
DateTime? releaseDate = releaseDateString != null
|
||||||
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
? DateTime.parse(releaseDateString)
|
||||||
return APKDetails(e['tag_name'] ?? e['name'], apkUrls.entries.toList(),
|
: null;
|
||||||
|
return APKDetails(
|
||||||
|
e['tag_name'] ?? e['name'],
|
||||||
|
apkUrls.entries.toList(),
|
||||||
AppNames(names.author, names.name.split('/').last),
|
AppNames(names.author, names.name.split('/').last),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
@ -208,8 +231,9 @@ class GitLab extends AppSource {
|
|||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
|
if (finalResult.apkUrls.isEmpty && fallbackToOlderReleases && !trackOnly) {
|
||||||
apkDetailsList =
|
apkDetailsList = apkDetailsList
|
||||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
.where((e) => e.apkUrls.isNotEmpty)
|
||||||
|
.toList();
|
||||||
finalResult = apkDetailsList.first;
|
finalResult = apkDetailsList.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,10 +242,13 @@ class GitLab extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
||||||
if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+')
|
if (RegExp(
|
||||||
.hasMatch(apkUrl.value)) {
|
'^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+',
|
||||||
|
).hasMatch(apkUrl.value)) {
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
|
apkUrl.key,
|
||||||
|
apkUrl.value.replaceFirst('/file/', '/raw/'),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,13 @@ import 'package:obtainium/providers/apps_provider.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||||
|
try {
|
||||||
|
if (Uri.parse(ambiguousUrl).isAbsolute) {
|
||||||
|
return ambiguousUrl; // #2315
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
return referenceAbsoluteUrl.resolve(ambiguousUrl).toString();
|
return referenceAbsoluteUrl.resolve(ambiguousUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,28 +100,37 @@ bool _isNumeric(String s) {
|
|||||||
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MapEntry<String, String>> getLinksInLines(String lines) => RegExp(
|
List<MapEntry<String, String>> getLinksInLines(String lines) =>
|
||||||
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
|
RegExp(
|
||||||
|
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?',
|
||||||
|
)
|
||||||
.allMatches(lines)
|
.allMatches(lines)
|
||||||
.map((match) =>
|
.map(
|
||||||
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
|
(match) =>
|
||||||
|
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Given an HTTP response, grab some links according to the common additional settings
|
// Given an HTTP response, grab some links according to the common additional settings
|
||||||
// (those that apply to intermediate and final steps)
|
// (those that apply to intermediate and final steps)
|
||||||
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
Future<List<MapEntry<String, String>>> grabLinksCommon(
|
||||||
Response res, Map<String, dynamic> additionalSettings) async {
|
Response res,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var html = parse(res.body);
|
var html = parse(res.body);
|
||||||
List<MapEntry<String, String>> allLinks = html
|
List<MapEntry<String, String>> allLinks = html
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((element) => MapEntry(
|
.map(
|
||||||
|
(element) => MapEntry(
|
||||||
element.attributes['href'] ?? '',
|
element.attributes['href'] ?? '',
|
||||||
element.text.isNotEmpty
|
element.text.isNotEmpty
|
||||||
? element.text
|
? element.text
|
||||||
: (element.attributes['href'] ?? '').split('/').last))
|
: (element.attributes['href'] ?? '').split('/').last,
|
||||||
|
),
|
||||||
|
)
|
||||||
.where((element) => element.key.isNotEmpty)
|
.where((element) => element.key.isNotEmpty)
|
||||||
.map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
.map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
|
||||||
.toList();
|
.toList();
|
||||||
@ -127,9 +143,13 @@ Future<List<MapEntry<String, String>>> grabLinksCommon(
|
|||||||
var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body));
|
var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body));
|
||||||
allLinks = getLinksInLines(jsonStrings.join('\n'));
|
allLinks = getLinksInLines(jsonStrings.join('\n'));
|
||||||
if (allLinks.isEmpty) {
|
if (allLinks.isEmpty) {
|
||||||
allLinks = getLinksInLines(jsonStrings.map((l) {
|
allLinks = getLinksInLines(
|
||||||
|
jsonStrings
|
||||||
|
.map((l) {
|
||||||
return ensureAbsoluteUrl(l, res.request!.url);
|
return ensureAbsoluteUrl(l, res.request!.url);
|
||||||
}).join('\n'));
|
})
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
@ -158,17 +178,20 @@ Future<List<MapEntry<String, String>>> grabLinksCommon(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Some links may not have valid encoding
|
// Some links may not have valid encoding
|
||||||
}
|
}
|
||||||
return Uri.parse(filterLinkByText ? element.value : link)
|
return Uri.parse(
|
||||||
.path
|
filterLinkByText ? element.value : link,
|
||||||
.toLowerCase()
|
).path.toLowerCase().endsWith('.apk');
|
||||||
.endsWith('.apk');
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
if (!skipSort) {
|
if (!skipSort) {
|
||||||
links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
|
links.sort(
|
||||||
? compareAlphaNumeric(a.key.split('/').where((e) => e.isNotEmpty).last,
|
(a, b) => additionalSettings['sortByLastLinkSegment'] == true
|
||||||
b.key.split('/').where((e) => e.isNotEmpty).last)
|
? compareAlphaNumeric(
|
||||||
: compareAlphaNumeric(a.key, b.key));
|
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) {
|
if (additionalSettings['reverseSort'] == true) {
|
||||||
links = links.reversed.toList();
|
links = links.reversed.toList();
|
||||||
@ -194,49 +217,61 @@ class HTML extends AppSource {
|
|||||||
|
|
||||||
var finalStepFormitems = [
|
var finalStepFormitems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('customLinkFilterRegex',
|
GeneratedFormTextField(
|
||||||
|
'customLinkFilterRegex',
|
||||||
label: tr('customLinkFilterRegex'),
|
label: tr('customLinkFilterRegex'),
|
||||||
hint: 'download/(.*/)?(android|apk|mobile)',
|
hint: 'download/(.*/)?(android|apk|mobile)',
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('versionExtractWholePage',
|
GeneratedFormSwitch(
|
||||||
label: tr('versionExtractWholePage'))
|
'versionExtractWholePage',
|
||||||
]
|
label: tr('versionExtractWholePage'),
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
var commonFormItems = [
|
var commonFormItems = [
|
||||||
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
|
[GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
|
||||||
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
[GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
|
||||||
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('sortByLastLinkSegment',
|
GeneratedFormSwitch(
|
||||||
label: tr('sortByLastLinkSegment'))
|
'sortByLastLinkSegment',
|
||||||
|
label: tr('sortByLastLinkSegment'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
var intermediateFormItems = [
|
var intermediateFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('customLinkFilterRegex',
|
GeneratedFormTextField(
|
||||||
|
'customLinkFilterRegex',
|
||||||
label: tr('intermediateLinkRegex'),
|
label: tr('intermediateLinkRegex'),
|
||||||
hint: '([0-9]+.)*[0-9]+/\$',
|
hint: '([0-9]+.)*[0-9]+/\$',
|
||||||
required: true,
|
required: true,
|
||||||
additionalValidators: [(value) => regExValidator(value)])
|
additionalValidators: [(value) => regExValidator(value)],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('autoLinkFilterByArch',
|
GeneratedFormSwitch(
|
||||||
label: tr('autoLinkFilterByArch'), defaultValue: false)
|
'autoLinkFilterByArch',
|
||||||
|
label: tr('autoLinkFilterByArch'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
HTML() {
|
HTML() {
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSubForm(
|
GeneratedFormSubForm('intermediateLink', [
|
||||||
'intermediateLink', [...intermediateFormItems, ...commonFormItems],
|
...intermediateFormItems,
|
||||||
label: tr('intermediateLink'))
|
...commonFormItems,
|
||||||
|
], label: tr('intermediateLink')),
|
||||||
],
|
],
|
||||||
finalStepFormitems[0],
|
finalStepFormitems[0],
|
||||||
...commonFormItems,
|
...commonFormItems,
|
||||||
@ -246,7 +281,8 @@ class HTML extends AppSource {
|
|||||||
'requestHeader',
|
'requestHeader',
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('requestHeader',
|
GeneratedFormTextField(
|
||||||
|
'requestHeader',
|
||||||
label: tr('requestHeader'),
|
label: tr('requestHeader'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
@ -260,17 +296,19 @@ class HTML extends AppSource {
|
|||||||
return tr('invalidInput');
|
return tr('invalidInput');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
label: tr('requestHeader'),
|
label: tr('requestHeader'),
|
||||||
defaultValue: [
|
defaultValue: [
|
||||||
{
|
{
|
||||||
'requestHeader':
|
'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'
|
'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(
|
GeneratedFormDropdown(
|
||||||
@ -278,18 +316,20 @@ class HTML extends AppSource {
|
|||||||
[
|
[
|
||||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
MapEntry('APKLinkHash', tr('APKLinkHash')),
|
MapEntry('APKLinkHash', tr('APKLinkHash')),
|
||||||
MapEntry('ETag', 'ETag')
|
MapEntry('ETag', 'ETag'),
|
||||||
],
|
],
|
||||||
label: tr('defaultPseudoVersioningMethod'),
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
defaultValue: 'partialAPKHash')
|
defaultValue: 'partialAPKHash',
|
||||||
]
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
if (additionalSettings.isNotEmpty) {
|
if (additionalSettings.isNotEmpty) {
|
||||||
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
if (additionalSettings['requestHeader']?.isNotEmpty != true) {
|
||||||
additionalSettings['requestHeader'] = [];
|
additionalSettings['requestHeader'] = [];
|
||||||
@ -330,7 +370,8 @@ class HTML extends AppSource {
|
|||||||
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
|
||||||
var intLinks = await grabLinksCommon(
|
var intLinks = await grabLinksCommon(
|
||||||
await sourceRequest(currentUrl, additionalSettings),
|
await sourceRequest(currentUrl, additionalSettings),
|
||||||
additionalSettings['intermediateLink'][i]);
|
additionalSettings['intermediateLink'][i],
|
||||||
|
);
|
||||||
if (intLinks.isEmpty) {
|
if (intLinks.isEmpty) {
|
||||||
throw NoReleasesError(note: currentUrl);
|
throw NoReleasesError(note: currentUrl);
|
||||||
} else {
|
} else {
|
||||||
@ -346,11 +387,17 @@ class HTML extends AppSource {
|
|||||||
String versionExtractionWholePageString = currentUrl;
|
String versionExtractionWholePageString = currentUrl;
|
||||||
if (additionalSettings['directAPKLink'] != true) {
|
if (additionalSettings['directAPKLink'] != true) {
|
||||||
Response res = await sourceRequest(currentUrl, additionalSettings);
|
Response res = await sourceRequest(currentUrl, additionalSettings);
|
||||||
versionExtractionWholePageString =
|
versionExtractionWholePageString = res.body
|
||||||
res.body.split('\r\n').join('\n').split('\n').join('\\n');
|
.split('\r\n')
|
||||||
|
.join('\n')
|
||||||
|
.split('\n')
|
||||||
|
.join('\\n');
|
||||||
links = await grabLinksCommon(res, additionalSettings);
|
links = await grabLinksCommon(res, additionalSettings);
|
||||||
links = filterApks(links, additionalSettings['apkFilterRegEx'],
|
links = filterApks(
|
||||||
additionalSettings['invertAPKFilter']);
|
links,
|
||||||
|
additionalSettings['apkFilterRegEx'],
|
||||||
|
additionalSettings['invertAPKFilter'],
|
||||||
|
);
|
||||||
if (links.isEmpty) {
|
if (links.isEmpty) {
|
||||||
throw NoReleasesError(note: currentUrl);
|
throw NoReleasesError(note: currentUrl);
|
||||||
}
|
}
|
||||||
@ -370,14 +417,19 @@ class HTML extends AppSource {
|
|||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
additionalSettings['versionExtractWholePage'] == true
|
additionalSettings['versionExtractWholePage'] == true
|
||||||
? versionExtractionWholePageString
|
? versionExtractionWholePageString
|
||||||
: relDecoded);
|
: relDecoded,
|
||||||
var apkReqHeaders =
|
);
|
||||||
await getRequestHeaders(additionalSettings, forAPKDownload: true);
|
var apkReqHeaders = await getRequestHeaders(
|
||||||
|
additionalSettings,
|
||||||
|
forAPKDownload: true,
|
||||||
|
);
|
||||||
if (version == null &&
|
if (version == null &&
|
||||||
additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') {
|
additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') {
|
||||||
version = await checkETagHeader(rel,
|
version = await checkETagHeader(
|
||||||
|
rel,
|
||||||
headers: apkReqHeaders,
|
headers: apkReqHeaders,
|
||||||
allowInsecure: additionalSettings['allowInsecure'] == true);
|
allowInsecure: additionalSettings['allowInsecure'] == true,
|
||||||
|
);
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -385,18 +437,21 @@ class HTML extends AppSource {
|
|||||||
version ??=
|
version ??=
|
||||||
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
||||||
? rel.hashCode.toString()
|
? rel.hashCode.toString()
|
||||||
: (await checkPartialDownloadHashDynamic(rel,
|
: (await checkPartialDownloadHashDynamic(
|
||||||
|
rel,
|
||||||
headers: apkReqHeaders,
|
headers: apkReqHeaders,
|
||||||
allowInsecure: additionalSettings['allowInsecure'] == true))
|
allowInsecure: additionalSettings['allowInsecure'] == true,
|
||||||
.toString();
|
)).toString();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
[rel].map((e) {
|
[rel].map((e) {
|
||||||
var uri = Uri.parse(e);
|
var uri = Uri.parse(e);
|
||||||
var fileName =
|
var fileName = uri.pathSegments.isNotEmpty
|
||||||
uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.origin;
|
? uri.pathSegments.last
|
||||||
|
: uri.origin;
|
||||||
return MapEntry('${e.hashCode}-$fileName', e);
|
return MapEntry('${e.hashCode}-$fileName', e);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
AppNames(uri.host, tr('app')));
|
AppNames(uri.host, tr('app')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}(/#)?/(app|appdl)/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -23,13 +24,18 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
return match.group(0)!;
|
return match.group(0)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDlUrl(String standardUrl) =>
|
String getDlUrl(String standardUrl) =>
|
||||||
'https://${hosts[0].replaceAll('appgallery.huawei', 'appgallery.cloud.huawei')}/appdl/${standardUrl.split('/').last}';
|
'https://${hosts[0].replaceAll('appgallery.huawei', 'appgallery.cloud.huawei')}/appdl/${standardUrl.split('/').last}';
|
||||||
|
|
||||||
requestAppdlRedirect(
|
Future<Response> requestAppdlRedirect(
|
||||||
String dlUrl, Map<String, dynamic> additionalSettings) async {
|
String dlUrl,
|
||||||
Response res =
|
Map<String, dynamic> additionalSettings,
|
||||||
await sourceRequest(dlUrl, additionalSettings, followRedirects: false);
|
) async {
|
||||||
|
Response res = await sourceRequest(
|
||||||
|
dlUrl,
|
||||||
|
additionalSettings,
|
||||||
|
followRedirects: false,
|
||||||
|
);
|
||||||
if (res.statusCode == 200 ||
|
if (res.statusCode == 200 ||
|
||||||
res.statusCode == 302 ||
|
res.statusCode == 302 ||
|
||||||
res.statusCode == 304) {
|
res.statusCode == 304) {
|
||||||
@ -39,7 +45,7 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appIdFromRedirectDlUrl(String redirectDlUrl) {
|
String appIdFromRedirectDlUrl(String redirectDlUrl) {
|
||||||
var parts = redirectDlUrl
|
var parts = redirectDlUrl
|
||||||
.split('?')[0]
|
.split('?')[0]
|
||||||
.split('/')
|
.split('/')
|
||||||
@ -53,8 +59,10 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
String dlUrl = getDlUrl(standardUrl);
|
String dlUrl = getDlUrl(standardUrl);
|
||||||
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
|
||||||
return res.headers['location'] != null
|
return res.headers['location'] != null
|
||||||
@ -76,8 +84,11 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
if (appId.isEmpty) {
|
if (appId.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var relDateStr =
|
var relDateStr = res.headers['location']
|
||||||
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
|
?.split('?')[0]
|
||||||
|
.split('.')
|
||||||
|
.reversed
|
||||||
|
.toList()[1];
|
||||||
if (relDateStr == null || relDateStr.length != 10) {
|
if (relDateStr == null || relDateStr.length != 10) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -88,10 +99,15 @@ class HuaweiAppGallery extends AppSource {
|
|||||||
relDateStrAdj.insert((i + i ~/ 2 - 1), '-');
|
relDateStrAdj.insert((i + i ~/ 2 - 1), '-');
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
var relDate =
|
var relDate = DateFormat(
|
||||||
DateFormat('yy-MM-dd-HH-mm', 'en_US').parse(relDateStrAdj.join(''));
|
'yy-MM-dd-HH-mm',
|
||||||
|
'en_US',
|
||||||
|
).parse(relDateStrAdj.join(''));
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId),
|
relDateStr,
|
||||||
releaseDate: relDate);
|
[MapEntry('$appId.apk', dlUrl)],
|
||||||
|
AppNames(name, appId),
|
||||||
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,14 @@ class IzzyOnDroid extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
'^https?://android.${getSourceRegex(hosts)}/repo/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
'^https?://apt.${getSourceRegex(hosts)}/fdroid/index/apk/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExB.firstMatch(url);
|
match = standardUrlRegExB.firstMatch(url);
|
||||||
}
|
}
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -32,8 +34,10 @@ class IzzyOnDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return fd.tryInferringAppId(standardUrl);
|
return fd.tryInferringAppId(standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,10 +50,12 @@ class IzzyOnDroid extends AppSource {
|
|||||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId',
|
||||||
additionalSettings),
|
additionalSettings,
|
||||||
|
),
|
||||||
'https://android.izzysoft.de/frepo/$appId',
|
'https://android.izzysoft.de/frepo/$appId',
|
||||||
standardUrl,
|
standardUrl,
|
||||||
name,
|
name,
|
||||||
additionalSettings: additionalSettings);
|
additionalSettings: additionalSettings,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,17 @@ class Jenkins extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
standardUrl = trimJobUrl(standardUrl);
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
|
'$standardUrl/lastSuccessfulBuild/api/json',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
var releaseDate = json['timestamp'] == null
|
var releaseDate = json['timestamp'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||||
var version =
|
var version = json['number'] == null
|
||||||
json['number'] == null ? null : (json['number'] as int).toString();
|
? null
|
||||||
|
: (json['number'] as int).toString();
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -51,16 +54,21 @@ class Jenkins extends AppSource {
|
|||||||
return path == null
|
return path == null
|
||||||
? const MapEntry<String, String>('', '')
|
? const MapEntry<String, String>('', '')
|
||||||
: MapEntry<String, String>(
|
: MapEntry<String, String>(
|
||||||
(e['fileName'] ?? e['relativePath']) as String, path);
|
(e['fileName'] ?? e['relativePath']) as String,
|
||||||
|
path,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.where((url) =>
|
.where(
|
||||||
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
(url) =>
|
||||||
|
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrls,
|
apkUrls,
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
|
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@ class Mullvad extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -31,7 +32,9 @@ class Mullvad extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'$standardUrl/en/download/android', additionalSettings);
|
'$standardUrl/en/download/android',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var versions = parse(res.body)
|
var versions = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
@ -54,8 +57,8 @@ class Mullvad extends AppSource {
|
|||||||
try {
|
try {
|
||||||
changeLog = (await GitHub().getLatestAPKDetails(
|
changeLog = (await GitHub().getLatestAPKDetails(
|
||||||
'https://github.com/mullvad/mullvadvpn-app',
|
'https://github.com/mullvad/mullvadvpn-app',
|
||||||
{'fallbackToOlderReleases': true}))
|
{'fallbackToOlderReleases': true},
|
||||||
.changeLog;
|
)).changeLog;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
@ -63,7 +66,8 @@ class Mullvad extends AppSource {
|
|||||||
versions[0],
|
versions[0],
|
||||||
getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
|
getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
|
||||||
AppNames(name, 'Mullvad-VPN'),
|
AppNames(name, 'Mullvad-VPN'),
|
||||||
changeLog: changeLog);
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@ class NeutronCode extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/downloads/file/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -55,7 +56,7 @@ class NeutronCode extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customDateParse(String dateString) {
|
String? customDateParse(String dateString) {
|
||||||
List<String> parts = dateString.split(' ');
|
List<String> parts = dateString.split(' ');
|
||||||
if (parts.length != 3) {
|
if (parts.length != 3) {
|
||||||
return null;
|
return null;
|
||||||
@ -89,24 +90,31 @@ class NeutronCode extends AppSource {
|
|||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var version =
|
var version = http
|
||||||
http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml;
|
.querySelector('.pd-version-txt')
|
||||||
|
?.nextElementSibling
|
||||||
|
?.innerHtml;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
String? apkUrl = 'https://${hosts[0]}/download/$filename';
|
||||||
var dateStringOriginal =
|
var dateStringOriginal = http
|
||||||
http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml;
|
.querySelector('.pd-date-txt')
|
||||||
|
?.nextElementSibling
|
||||||
|
?.innerHtml;
|
||||||
var dateString = dateStringOriginal != null
|
var dateString = dateStringOriginal != null
|
||||||
? (customDateParse(dateStringOriginal))
|
? (customDateParse(dateStringOriginal))
|
||||||
: null;
|
: null;
|
||||||
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
||||||
return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
getApkUrlsFromUrls([apkUrl]),
|
||||||
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
||||||
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
||||||
changeLog: changeLogElements.isNotEmpty
|
changeLog: changeLogElements.isNotEmpty
|
||||||
? changeLogElements.last.innerHtml
|
? changeLogElements.last.innerHtml
|
||||||
: null);
|
: null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,8 @@ class RuStore extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/catalog/app/+[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -28,16 +29,18 @@ class RuStore extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> decodeString(String str) async {
|
Future<String> decodeString(String str) async {
|
||||||
try {
|
try {
|
||||||
return (await CharsetDetector.autoDecode(
|
return (await CharsetDetector.autoDecode(
|
||||||
Uint8List.fromList(str.codeUnits)))
|
Uint8List.fromList(str.codeUnits),
|
||||||
.string;
|
)).string;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@ -51,7 +54,8 @@ class RuStore extends AppSource {
|
|||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
Response res0 = await sourceRequest(
|
Response res0 = await sourceRequest(
|
||||||
'https://backapi.rustore.ru/applicationData/overallInfo/$appId',
|
'https://backapi.rustore.ru/applicationData/overallInfo/$appId',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res0.statusCode != 200) {
|
if (res0.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res0);
|
throw getObtainiumHttpError(res0);
|
||||||
}
|
}
|
||||||
@ -77,7 +81,8 @@ class RuStore extends AppSource {
|
|||||||
'https://backapi.rustore.ru/applicationData/download-link',
|
'https://backapi.rustore.ru/applicationData/download-link',
|
||||||
additionalSettings,
|
additionalSettings,
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
postBody: {"appId": appDetails['appId'], "firstInstall": true});
|
postBody: {"appId": appDetails['appId'], "firstInstall": true},
|
||||||
|
);
|
||||||
var downloadDetails = jsonDecode(res1.body)['body'];
|
var downloadDetails = jsonDecode(res1.body)['body'];
|
||||||
if (res1.statusCode != 200 || downloadDetails['apkUrl'] == null) {
|
if (res1.statusCode != 200 || downloadDetails['apkUrl'] == null) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
@ -90,11 +95,14 @@ class RuStore extends AppSource {
|
|||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
getApkUrlsFromUrls([
|
getApkUrlsFromUrls([
|
||||||
(downloadDetails['apkUrl'] as String)
|
(downloadDetails['apkUrl'] as String).replaceAll(
|
||||||
.replaceAll(RegExp('\\.zip\$'), '.apk')
|
RegExp('\\.zip\$'),
|
||||||
|
'.apk',
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
AppNames(author, appName),
|
AppNames(author, appName),
|
||||||
releaseDate: relDate,
|
releaseDate: relDate,
|
||||||
changeLog: changeLog);
|
changeLog: changeLog,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,10 @@ class SourceForge extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
var sourceRegex = getSourceRegex(hosts);
|
var sourceRegex = getSourceRegex(hosts);
|
||||||
RegExp standardUrlRegExC =
|
RegExp standardUrlRegExC = RegExp(
|
||||||
RegExp('^https?://(www\\.)?$sourceRegex/p/.+', caseSensitive: false);
|
'^https?://(www\\.)?$sourceRegex/p/.+',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegExC.firstMatch(url);
|
RegExpMatch? match = standardUrlRegExC.firstMatch(url);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url =
|
url =
|
||||||
@ -20,14 +22,16 @@ class SourceForge extends AppSource {
|
|||||||
}
|
}
|
||||||
RegExp standardUrlRegExB = RegExp(
|
RegExp standardUrlRegExB = RegExp(
|
||||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+',
|
'^https?://(www\\.)?$sourceRegex/projects/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExB.firstMatch(url);
|
match = standardUrlRegExB.firstMatch(url);
|
||||||
if (match != null && match.group(0) == url) {
|
if (match != null && match.group(0) == url) {
|
||||||
url = '$url/files';
|
url = '$url/files';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp(
|
RegExp standardUrlRegExA = RegExp(
|
||||||
'^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?',
|
'^https?://(www\\.)?$sourceRegex/projects/[^/]+/files(/.+)?',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
match = standardUrlRegExA.firstMatch(url);
|
match = standardUrlRegExA.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -47,7 +51,8 @@ class SourceForge extends AppSource {
|
|||||||
}
|
}
|
||||||
Response res = await sourceRequest(
|
Response res = await sourceRequest(
|
||||||
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
|
||||||
additionalSettings);
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var allDownloadLinks = parsedHtml
|
var allDownloadLinks = parsedHtml
|
||||||
@ -76,7 +81,8 @@ class SourceForge extends AppSource {
|
|||||||
var extractedVersion = extractVersion(
|
var extractedVersion = extractVersion(
|
||||||
additionalSettings['versionExtractionRegEx'] as String?,
|
additionalSettings['versionExtractionRegEx'] as String?,
|
||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
version);
|
version,
|
||||||
|
);
|
||||||
if (extractedVersion != null) {
|
if (extractedVersion != null) {
|
||||||
version = extractedVersion;
|
version = extractedVersion;
|
||||||
}
|
}
|
||||||
@ -111,8 +117,11 @@ class SourceForge extends AppSource {
|
|||||||
.where((element) => getVersion(element) == version)
|
.where((element) => getVersion(element) == version)
|
||||||
.toList();
|
.toList();
|
||||||
var segments = standardUrl.split('/');
|
var segments = standardUrl.split('/');
|
||||||
return APKDetails(version, getApkUrlsFromUrls(apkUrlList),
|
return APKDetails(
|
||||||
AppNames(name, segments[segments.indexOf('files') - 1]));
|
version,
|
||||||
|
getApkUrlsFromUrls(apkUrlList),
|
||||||
|
AppNames(name, segments[segments.indexOf('files') - 1]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,12 @@ class SourceHut extends AppSource {
|
|||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
GeneratedFormSwitch(
|
||||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
'fallbackToOlderReleases',
|
||||||
]
|
label: tr('fallbackToOlderReleases'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +26,8 @@ class SourceHut extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
'^https?://(www\\.)?${getSourceRegex(hosts)}/[^/]+/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -52,8 +56,10 @@ class SourceHut extends AppSource {
|
|||||||
String appName = standardUri.pathSegments.last;
|
String appName = standardUri.pathSegments.last;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
|
'$standardUrl/refs/rss.xml',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
List<APKDetails> apkDetailsList = [];
|
List<APKDetails> apkDetailsList = [];
|
||||||
@ -84,8 +90,9 @@ class SourceHut extends AppSource {
|
|||||||
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||||
: null;
|
: null;
|
||||||
releaseDate = releaseDateString != null
|
releaseDate = releaseDateString != null
|
||||||
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z')
|
? DateFormat(
|
||||||
.parse(releaseDateString)
|
'EEE, dd MMM yyyy HH:mm:ss Z',
|
||||||
|
).parse(releaseDateString)
|
||||||
: null;
|
: null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
@ -93,27 +100,35 @@ class SourceHut extends AppSource {
|
|||||||
var res2 = await sourceRequest(releasePage, additionalSettings);
|
var res2 = await sourceRequest(releasePage, additionalSettings);
|
||||||
List<MapEntry<String, String>> apkUrls = [];
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
if (res2.statusCode == 200) {
|
if (res2.statusCode == 200) {
|
||||||
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
apkUrls = getApkUrlsFromUrls(
|
||||||
|
parse(res2.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((e) => e.attributes['href'] ?? '')
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
.where((e) => e.toLowerCase().endsWith('.apk'))
|
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||||
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
||||||
.toList());
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
apkDetailsList.add(APKDetails(
|
apkDetailsList.add(
|
||||||
|
APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrls,
|
apkUrls,
|
||||||
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
AppNames(
|
||||||
appName),
|
entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
||||||
releaseDate: releaseDate));
|
appName,
|
||||||
|
),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
if (fallbackToOlderReleases) {
|
if (fallbackToOlderReleases) {
|
||||||
if (additionalSettings['trackOnly'] != true) {
|
if (additionalSettings['trackOnly'] != true) {
|
||||||
apkDetailsList =
|
apkDetailsList = apkDetailsList
|
||||||
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
.where((e) => e.apkUrls.isNotEmpty)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
if (apkDetailsList.isEmpty) {
|
if (apkDetailsList.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
|
@ -20,12 +20,15 @@ class TelegramApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res =
|
Response res = await sourceRequest(
|
||||||
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
|
'https://t.me/s/TAndroidAPK',
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var messages =
|
var messages = http.querySelectorAll(
|
||||||
http.querySelectorAll('.tgme_widget_message_text.js-message_text');
|
'.tgme_widget_message_text.js-message_text',
|
||||||
|
);
|
||||||
var version = messages.isNotEmpty
|
var version = messages.isNotEmpty
|
||||||
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
|
? messages.last.innerHtml.split('\n').first.trim().split(' ').first
|
||||||
: null;
|
: null;
|
||||||
@ -33,10 +36,9 @@ class TelegramApp extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
||||||
return APKDetails(
|
return APKDetails(version, [
|
||||||
version,
|
MapEntry<String, String>('telegram-$version.apk', apkUrl),
|
||||||
[MapEntry<String, String>('telegram-$version.apk', apkUrl)],
|
], AppNames('Telegram', 'Telegram'));
|
||||||
AppNames('Telegram', 'Telegram'));
|
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@ class Tencent extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://${getSourceRegex(hosts)}/appdetail/[^/]+',
|
'^https?://${getSourceRegex(hosts)}/appdetail/[^/]+',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
var match = standardUrlRegEx.firstMatch(url);
|
var match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -25,8 +26,10 @@ class Tencent extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,18 +39,16 @@ class Tencent extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String appId = (await tryInferringAppId(standardUrl))!;
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
String baseHost = Uri.parse(standardUrl)
|
String baseHost = Uri.parse(
|
||||||
.host
|
standardUrl,
|
||||||
.split('.')
|
).host.split('.').reversed.toList().sublist(0, 2).reversed.join('.');
|
||||||
.reversed
|
|
||||||
.toList()
|
|
||||||
.sublist(0, 2)
|
|
||||||
.reversed
|
|
||||||
.join('.');
|
|
||||||
|
|
||||||
var res = await sourceRequest(
|
var res = await sourceRequest(
|
||||||
'https://upage.html5.$baseHost/wechat-apkinfo', additionalSettings,
|
'https://upage.html5.$baseHost/wechat-apkinfo',
|
||||||
followRedirects: false, postBody: {"packagename": appId});
|
additionalSettings,
|
||||||
|
followRedirects: false,
|
||||||
|
postBody: {"packagename": appId},
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
@ -64,14 +65,18 @@ class Tencent extends AppSource {
|
|||||||
var author = json['app_detail_records'][appId]['app_info']['author'];
|
var author = json['app_detail_records'][appId]['app_info']['author'];
|
||||||
var releaseDate =
|
var releaseDate =
|
||||||
json['app_detail_records'][appId]['app_info']['update_time'];
|
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';
|
'${appId}_$version.apk';
|
||||||
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, [MapEntry(apkName, apkUrl)], AppNames(author, appName),
|
version,
|
||||||
|
[MapEntry(apkName, apkUrl)],
|
||||||
|
AppNames(author, appName),
|
||||||
releaseDate: releaseDate != null
|
releaseDate: releaseDate != null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000)
|
? DateTime.fromMillisecondsSinceEpoch(releaseDate * 1000)
|
||||||
: null);
|
: null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import 'package:html/parser.dart';
|
|||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
parseDateTimeMMMddCommayyyy(String? dateString) {
|
DateTime? parseDateTimeMMMddCommayyyy(String? dateString) {
|
||||||
DateTime? releaseDate;
|
DateTime? releaseDate;
|
||||||
try {
|
try {
|
||||||
releaseDate = dateString != null
|
releaseDate = dateString != null
|
||||||
@ -31,7 +31,8 @@ class Uptodown extends AppSource {
|
|||||||
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
||||||
RegExp standardUrlRegEx = RegExp(
|
RegExp standardUrlRegEx = RegExp(
|
||||||
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
'^https?://([^\\.]+\\.){2,}${getSourceRegex(hosts)}',
|
||||||
caseSensitive: false);
|
caseSensitive: false,
|
||||||
|
);
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
@ -40,14 +41,20 @@ class Uptodown extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return (await getAppDetailsFromPage(
|
return (await getAppDetailsFromPage(
|
||||||
standardUrl, additionalSettings))['appId'];
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
))['appId'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String?>> getAppDetailsFromPage(
|
Future<Map<String, String?>> getAppDetailsFromPage(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(standardUrl, additionalSettings);
|
var res = await sourceRequest(standardUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -63,8 +70,9 @@ class Uptodown extends AppSource {
|
|||||||
.toList();
|
.toList();
|
||||||
String? appId = detailElements.elementAtOrNull(0);
|
String? appId = detailElements.elementAtOrNull(0);
|
||||||
String? dateStr = detailElements.elementAtOrNull(6);
|
String? dateStr = detailElements.elementAtOrNull(6);
|
||||||
String? fileId =
|
String? fileId = html
|
||||||
html.querySelector('#detail-app-name')?.attributes['data-file-id'];
|
.querySelector('#detail-app-name')
|
||||||
|
?.attributes['data-file-id'];
|
||||||
String? extension = detailElements.elementAtOrNull(7)?.toLowerCase();
|
String? extension = detailElements.elementAtOrNull(7)?.toLowerCase();
|
||||||
return Map.fromEntries([
|
return Map.fromEntries([
|
||||||
MapEntry('version', version),
|
MapEntry('version', version),
|
||||||
@ -73,7 +81,7 @@ class Uptodown extends AppSource {
|
|||||||
MapEntry('author', author),
|
MapEntry('author', author),
|
||||||
MapEntry('dateStr', dateStr),
|
MapEntry('dateStr', dateStr),
|
||||||
MapEntry('fileId', fileId),
|
MapEntry('fileId', fileId),
|
||||||
MapEntry('extension', extension)
|
MapEntry('extension', extension),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,8 +90,10 @@ class Uptodown extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var appDetails =
|
var appDetails = await getAppDetailsFromPage(
|
||||||
await getAppDetailsFromPage(standardUrl, additionalSettings);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
var version = appDetails['version'];
|
var version = appDetails['version'];
|
||||||
var appId = appDetails['appId'];
|
var appId = appDetails['appId'];
|
||||||
var fileId = appDetails['fileId'];
|
var fileId = appDetails['fileId'];
|
||||||
@ -105,21 +115,28 @@ class Uptodown extends AppSource {
|
|||||||
if (dateStr != null) {
|
if (dateStr != null) {
|
||||||
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
||||||
}
|
}
|
||||||
return APKDetails(version, [MapEntry('$appId.$extension', apkUrl)],
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
[MapEntry('$appId.$extension', apkUrl)],
|
||||||
AppNames(author, appName),
|
AppNames(author, appName),
|
||||||
releaseDate: relDate);
|
releaseDate: relDate,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var res = await sourceRequest(apkUrl, additionalSettings);
|
var res = await sourceRequest(apkUrl, additionalSettings);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var html = parse(res.body);
|
var html = parse(res.body);
|
||||||
var finalUrlKey =
|
var finalUrlKey = html
|
||||||
html.querySelector('#detail-download-button')?.attributes['data-url'];
|
.querySelector('#detail-download-button')
|
||||||
|
?.attributes['data-url'];
|
||||||
if (finalUrlKey == null) {
|
if (finalUrlKey == null) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,19 @@ class VivoAppStore extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
var json = await getDetailJson(standardUrl, additionalSettings);
|
var json = await getDetailJson(standardUrl, additionalSettings);
|
||||||
return json['package_name'];
|
return json['package_name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var json = await getDetailJson(standardUrl, additionalSettings);
|
var json = await getDetailJson(standardUrl, additionalSettings);
|
||||||
var appName = json['title_zh'].toString();
|
var appName = json['title_zh'].toString();
|
||||||
var packageName = json['package_name'].toString();
|
var packageName = json['package_name'].toString();
|
||||||
@ -42,13 +46,18 @@ class VivoAppStore extends AppSource {
|
|||||||
var apkUrl = json['download_url'].toString();
|
var apkUrl = json['download_url'].toString();
|
||||||
var apkName = '${packageName}_$versionCode.apk';
|
var apkName = '${packageName}_$versionCode.apk';
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
versionName, [MapEntry(apkName, apkUrl)], AppNames(developer, appName),
|
versionName,
|
||||||
releaseDate: DateTime.parse(uploadTime));
|
[MapEntry(apkName, apkUrl)],
|
||||||
|
AppNames(developer, appName),
|
||||||
|
releaseDate: DateTime.parse(uploadTime),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) async {
|
||||||
var apiBaseUrl =
|
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=';
|
'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)}';
|
var searchUrl = '$apiBaseUrl${Uri.encodeQueryComponent(query)}';
|
||||||
@ -65,14 +74,16 @@ class VivoAppStore extends AppSource {
|
|||||||
for (var item in (resultsJson as List<dynamic>)) {
|
for (var item in (resultsJson as List<dynamic>)) {
|
||||||
results['$appDetailUrl${item['id']}'] = [
|
results['$appDetailUrl${item['id']}'] = [
|
||||||
item['title_zh'].toString(),
|
item['title_zh'].toString(),
|
||||||
item['developer'].toString()
|
item['developer'].toString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getDetailJson(
|
Future<Map<String, dynamic>> getDetailJson(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
var vivoAppId = parseVivoAppId(standardUrl);
|
var vivoAppId = parseVivoAppId(standardUrl);
|
||||||
var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/';
|
var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/';
|
||||||
var params = '?frompage=messageh5&app_version=2100';
|
var params = '?frompage=messageh5&app_version=2100';
|
||||||
|
@ -20,8 +20,9 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
|||||||
titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
title: Text(
|
title: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(color: Theme.of(context).textTheme.bodyMedium!.color),
|
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -16,11 +16,13 @@ abstract class GeneratedFormItem {
|
|||||||
dynamic ensureType(dynamic val);
|
dynamic ensureType(dynamic val);
|
||||||
GeneratedFormItem clone();
|
GeneratedFormItem clone();
|
||||||
|
|
||||||
GeneratedFormItem(this.key,
|
GeneratedFormItem(
|
||||||
{this.label = 'Input',
|
this.key, {
|
||||||
|
this.label = 'Input',
|
||||||
this.belowWidgets = const [],
|
this.belowWidgets = const [],
|
||||||
this.defaultValue,
|
this.defaultValue,
|
||||||
this.additionalValidators = const []});
|
this.additionalValidators = const [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedFormTextField extends GeneratedFormItem {
|
class GeneratedFormTextField extends GeneratedFormItem {
|
||||||
@ -31,18 +33,19 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
late TextInputType? textInputType;
|
late TextInputType? textInputType;
|
||||||
late List<String>? autoCompleteOptions;
|
late List<String>? autoCompleteOptions;
|
||||||
|
|
||||||
GeneratedFormTextField(super.key,
|
GeneratedFormTextField(
|
||||||
{super.label,
|
super.key, {
|
||||||
|
super.label,
|
||||||
super.belowWidgets,
|
super.belowWidgets,
|
||||||
String super.defaultValue = '',
|
String super.defaultValue = '',
|
||||||
List<String? Function(String? value)> super.additionalValidators =
|
List<String? Function(String? value)> super.additionalValidators = const [],
|
||||||
const [],
|
|
||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.hint,
|
this.hint,
|
||||||
this.password = false,
|
this.password = false,
|
||||||
this.textInputType,
|
this.textInputType,
|
||||||
this.autoCompleteOptions});
|
this.autoCompleteOptions,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String ensureType(val) {
|
String ensureType(val) {
|
||||||
@ -51,7 +54,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormTextField clone() {
|
GeneratedFormTextField clone() {
|
||||||
return GeneratedFormTextField(key,
|
return GeneratedFormTextField(
|
||||||
|
key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
@ -60,7 +64,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
max: max,
|
max: max,
|
||||||
hint: hint,
|
hint: hint,
|
||||||
password: password,
|
password: password,
|
||||||
textInputType: textInputType);
|
textInputType: textInputType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,8 +96,9 @@ class GeneratedFormDropdown extends GeneratedFormItem {
|
|||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
disabledOptKeys:
|
disabledOptKeys: disabledOptKeys != null
|
||||||
disabledOptKeys != null ? List.from(disabledOptKeys!) : null,
|
? List.from(disabledOptKeys!)
|
||||||
|
: null,
|
||||||
additionalValidators: List.from(additionalValidators),
|
additionalValidators: List.from(additionalValidators),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,12 +123,14 @@ class GeneratedFormSwitch extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormSwitch clone() {
|
GeneratedFormSwitch clone() {
|
||||||
return GeneratedFormSwitch(key,
|
return GeneratedFormSwitch(
|
||||||
|
key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
additionalValidators: List.from(additionalValidators));
|
additionalValidators: List.from(additionalValidators),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,17 +140,20 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
late WrapAlignment alignment;
|
late WrapAlignment alignment;
|
||||||
late String emptyMessage;
|
late String emptyMessage;
|
||||||
late bool showLabelWhenNotEmpty;
|
late bool showLabelWhenNotEmpty;
|
||||||
GeneratedFormTagInput(super.key,
|
GeneratedFormTagInput(
|
||||||
{super.label,
|
super.key, {
|
||||||
|
super.label,
|
||||||
super.belowWidgets,
|
super.belowWidgets,
|
||||||
Map<String, MapEntry<int, bool>> super.defaultValue = const {},
|
Map<String, MapEntry<int, bool>> super.defaultValue = const {},
|
||||||
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
||||||
super.additionalValidators = const [],
|
super.additionalValidators =
|
||||||
|
const [],
|
||||||
this.deleteConfirmationMessage,
|
this.deleteConfirmationMessage,
|
||||||
this.singleSelect = false,
|
this.singleSelect = false,
|
||||||
this.alignment = WrapAlignment.start,
|
this.alignment = WrapAlignment.start,
|
||||||
this.emptyMessage = 'Input',
|
this.emptyMessage = 'Input',
|
||||||
this.showLabelWhenNotEmpty = true});
|
this.showLabelWhenNotEmpty = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, MapEntry<int, bool>> ensureType(val) {
|
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||||
@ -151,7 +162,8 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormTagInput clone() {
|
GeneratedFormTagInput clone() {
|
||||||
return GeneratedFormTagInput(key,
|
return GeneratedFormTagInput(
|
||||||
|
key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
@ -160,16 +172,20 @@ class GeneratedFormTagInput extends GeneratedFormItem {
|
|||||||
singleSelect: singleSelect,
|
singleSelect: singleSelect,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
emptyMessage: emptyMessage,
|
emptyMessage: emptyMessage,
|
||||||
showLabelWhenNotEmpty: showLabelWhenNotEmpty);
|
showLabelWhenNotEmpty: showLabelWhenNotEmpty,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef OnValueChanges = void Function(
|
typedef OnValueChanges =
|
||||||
Map<String, dynamic> values, bool valid, bool isBuilding);
|
void Function(Map<String, dynamic> values, bool valid, bool isBuilding);
|
||||||
|
|
||||||
class GeneratedForm extends StatefulWidget {
|
class GeneratedForm extends StatefulWidget {
|
||||||
const GeneratedForm(
|
const GeneratedForm({
|
||||||
{super.key, required this.items, required this.onValueChanges});
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
required this.onValueChanges,
|
||||||
|
});
|
||||||
|
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
final OnValueChanges onValueChanges;
|
final OnValueChanges onValueChanges;
|
||||||
@ -179,7 +195,8 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<List<GeneratedFormItem>> cloneFormItems(
|
List<List<GeneratedFormItem>> cloneFormItems(
|
||||||
List<List<GeneratedFormItem>> items) {
|
List<List<GeneratedFormItem>> items,
|
||||||
|
) {
|
||||||
List<List<GeneratedFormItem>> clonedItems = [];
|
List<List<GeneratedFormItem>> clonedItems = [];
|
||||||
for (var row in items) {
|
for (var row in items) {
|
||||||
List<GeneratedFormItem> clonedRow = [];
|
List<GeneratedFormItem> clonedRow = [];
|
||||||
@ -194,8 +211,13 @@ List<List<GeneratedFormItem>> cloneFormItems(
|
|||||||
class GeneratedFormSubForm extends GeneratedFormItem {
|
class GeneratedFormSubForm extends GeneratedFormItem {
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
|
|
||||||
GeneratedFormSubForm(super.key, this.items,
|
GeneratedFormSubForm(
|
||||||
{super.label, super.belowWidgets, super.defaultValue = const []});
|
super.key,
|
||||||
|
this.items, {
|
||||||
|
super.label,
|
||||||
|
super.belowWidgets,
|
||||||
|
super.defaultValue = const [],
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ensureType(val) {
|
ensureType(val) {
|
||||||
@ -204,8 +226,13 @@ class GeneratedFormSubForm extends GeneratedFormItem {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
GeneratedFormSubForm clone() {
|
GeneratedFormSubForm clone() {
|
||||||
return GeneratedFormSubForm(key, cloneFormItems(items),
|
return GeneratedFormSubForm(
|
||||||
label: label, belowWidgets: belowWidgets, defaultValue: defaultValue);
|
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
|
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
||||||
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
||||||
// Map RBG values from 0-1 to 0-255:
|
// Map RBG values from 0-1 to 0-255:
|
||||||
final List<int> rgbValues =
|
final List<int> rgbValues = rgbValuesDbl
|
||||||
rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
|
.map((rgb) => (rgb * 255).toInt())
|
||||||
|
.toList();
|
||||||
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
int generateRandomNumber(int seed1,
|
int generateRandomNumber(
|
||||||
{int seed2 = 0, int seed3 = 0, max = 10000}) {
|
int seed1, {
|
||||||
|
int seed2 = 0,
|
||||||
|
int seed3 = 0,
|
||||||
|
max = 10000,
|
||||||
|
}) {
|
||||||
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
|
int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
|
||||||
Random random = Random(combinedSeed);
|
Random random = Random(combinedSeed);
|
||||||
int randomNumber = random.nextInt(max);
|
int randomNumber = random.nextInt(max);
|
||||||
@ -261,7 +293,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
initForm() {
|
void initForm() {
|
||||||
initKey = widget.key.toString();
|
initKey = widget.key.toString();
|
||||||
// Initialize form values as all empty
|
// Initialize form values as all empty
|
||||||
values.clear();
|
values.clear();
|
||||||
@ -297,9 +329,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
helperText:
|
helperText: formItem.label + (formItem.required ? ' *' : ''),
|
||||||
formItem.label + (formItem.required ? ' *' : ''),
|
hintText: formItem.hint,
|
||||||
hintText: formItem.hint),
|
),
|
||||||
minLines: formItem.max <= 1 ? null : formItem.max,
|
minLines: formItem.max <= 1 ? null : formItem.max,
|
||||||
maxLines: formItem.max <= 1 ? 1 : formItem.max,
|
maxLines: formItem.max <= 1 ? 1 : formItem.max,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@ -342,20 +374,23 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
decoration: InputDecoration(labelText: formItem.label),
|
decoration: InputDecoration(labelText: formItem.label),
|
||||||
value: values[formItem.key],
|
value: values[formItem.key],
|
||||||
items: formItem.opts!.map((e2) {
|
items: formItem.opts!.map((e2) {
|
||||||
var enabled =
|
var enabled = formItem.disabledOptKeys?.contains(e2.key) != true;
|
||||||
formItem.disabledOptKeys?.contains(e2.key) != true;
|
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: e2.key,
|
value: e2.key,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: enabled ? 1 : 0.5, child: Text(e2.value)));
|
opacity: enabled ? 1 : 0.5,
|
||||||
|
child: Text(e2.value),
|
||||||
|
),
|
||||||
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
values[formItem.key] = value ?? formItem.opts!.first.key;
|
values[formItem.key] = value ?? formItem.opts!.first.key;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} else if (formItem is GeneratedFormSubForm) {
|
} else if (formItem is GeneratedFormSubForm) {
|
||||||
values[formItem.key] = [];
|
values[formItem.key] = [];
|
||||||
for (Map<String, dynamic> v
|
for (Map<String, dynamic> v
|
||||||
@ -394,20 +429,18 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Flexible(child: Text(widget.items[r][e].label)),
|
Flexible(child: Text(widget.items[r][e].label)),
|
||||||
const SizedBox(
|
const SizedBox(width: 8),
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Switch(
|
Switch(
|
||||||
value: values[fieldKey],
|
value: values[fieldKey],
|
||||||
onChanged:
|
onChanged: (widget.items[r][e] as GeneratedFormSwitch).disabled
|
||||||
(widget.items[r][e] as GeneratedFormSwitch).disabled
|
|
||||||
? null
|
? null
|
||||||
: (value) {
|
: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
values[fieldKey] = value;
|
values[fieldKey] = value;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
})
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
} else if (widget.items[r][e] is GeneratedFormTagInput) {
|
||||||
@ -418,9 +451,11 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: widget.items[r][e].label,
|
title: widget.items[r][e].label,
|
||||||
items: [
|
items: [
|
||||||
[GeneratedFormTextField('label', label: tr('label'))]
|
[GeneratedFormTextField('label', label: tr('label'))],
|
||||||
]);
|
],
|
||||||
}).then((value) {
|
);
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
String? label = value?['label'];
|
String? label = value?['label'];
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -434,8 +469,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
var someSelected = temp.entries
|
var someSelected = temp.entries
|
||||||
.where((element) => element.value.value)
|
.where((element) => element.value.value)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
temp[label] = MapEntry(generateRandomLightColor().value,
|
temp[label] = MapEntry(
|
||||||
!(someSelected && singleSelect));
|
generateRandomLightColor().value,
|
||||||
|
!(someSelected && singleSelect),
|
||||||
|
);
|
||||||
values[fieldKey] = temp;
|
values[fieldKey] = temp;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
}
|
}
|
||||||
@ -444,8 +481,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formInputs[r][e] =
|
formInputs[r][e] = Column(
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
true &&
|
true &&
|
||||||
@ -459,9 +497,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
: CrossAxisAlignment.stretch,
|
: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.items[r][e].label),
|
Text(widget.items[r][e].label),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
@ -481,45 +517,63 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
?.entries
|
?.entries
|
||||||
.map((e2) {
|
.map((e2) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
child: ChoiceChip(
|
child: ChoiceChip(
|
||||||
label: Text(e2.key),
|
label: Text(e2.key),
|
||||||
backgroundColor: Color(e2.value.key).withAlpha(50),
|
backgroundColor: Color(
|
||||||
|
e2.value.key,
|
||||||
|
).withAlpha(50),
|
||||||
selectedColor: Color(e2.value.key),
|
selectedColor: Color(e2.value.key),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
selected: e2.value.value,
|
selected: e2.value.value,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
(values[fieldKey] as Map<String,
|
(values[fieldKey]
|
||||||
MapEntry<int, bool>>)[e2.key] =
|
as Map<String, MapEntry<int, bool>>)[e2
|
||||||
MapEntry(
|
.key] = MapEntry(
|
||||||
(values[fieldKey] as Map<String,
|
(values[fieldKey]
|
||||||
MapEntry<int, bool>>)[e2.key]!
|
as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>
|
||||||
|
>)[e2.key]!
|
||||||
.key,
|
.key,
|
||||||
value);
|
value,
|
||||||
|
);
|
||||||
if ((widget.items[r][e]
|
if ((widget.items[r][e]
|
||||||
as GeneratedFormTagInput)
|
as GeneratedFormTagInput)
|
||||||
.singleSelect &&
|
.singleSelect &&
|
||||||
value == true) {
|
value == true) {
|
||||||
for (var key in (values[fieldKey]
|
for (var key
|
||||||
as Map<String, MapEntry<int, bool>>)
|
in (values[fieldKey]
|
||||||
|
as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>
|
||||||
|
>)
|
||||||
.keys) {
|
.keys) {
|
||||||
if (key != e2.key) {
|
if (key != e2.key) {
|
||||||
(values[fieldKey] as Map<
|
(values[fieldKey]
|
||||||
|
as Map<
|
||||||
String,
|
String,
|
||||||
MapEntry<int,
|
MapEntry<int, bool>
|
||||||
bool>>)[key] = MapEntry(
|
>)[key] = MapEntry(
|
||||||
(values[fieldKey] as Map<String,
|
(values[fieldKey]
|
||||||
MapEntry<int, bool>>)[key]!
|
as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>
|
||||||
|
>)[key]!
|
||||||
.key,
|
.key,
|
||||||
false);
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}) ??
|
}) ??
|
||||||
[const SizedBox.shrink()],
|
[const SizedBox.shrink()],
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
@ -532,19 +586,23 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
var temp = values[fieldKey]
|
var temp =
|
||||||
|
values[fieldKey]
|
||||||
as Map<String, MapEntry<int, bool>>;
|
as Map<String, MapEntry<int, bool>>;
|
||||||
// get selected category str where bool is true
|
// get selected category str where bool is true
|
||||||
final oldEntry = temp.entries
|
final oldEntry = temp.entries.firstWhere(
|
||||||
.firstWhere((entry) => entry.value.value);
|
(entry) => entry.value.value,
|
||||||
|
);
|
||||||
// generate new color, ensure it is not the same
|
// generate new color, ensure it is not the same
|
||||||
int newColor = oldEntry.value.key;
|
int newColor = oldEntry.value.key;
|
||||||
while (oldEntry.value.key == newColor) {
|
while (oldEntry.value.key == newColor) {
|
||||||
newColor = generateRandomLightColor().value;
|
newColor = generateRandomLightColor().value;
|
||||||
}
|
}
|
||||||
// Update entry with new color, remain selected
|
// Update entry with new color, remain selected
|
||||||
temp.update(oldEntry.key,
|
temp.update(
|
||||||
(old) => MapEntry(newColor, old.value));
|
oldEntry.key,
|
||||||
|
(old) => MapEntry(newColor, old.value),
|
||||||
|
);
|
||||||
values[fieldKey] = temp;
|
values[fieldKey] = temp;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
@ -552,7 +610,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
icon: const Icon(Icons.format_color_fill_rounded),
|
icon: const Icon(Icons.format_color_fill_rounded),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: tr('colour'),
|
tooltip: tr('colour'),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
?.values
|
?.values
|
||||||
@ -565,7 +624,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
fn() {
|
fn() {
|
||||||
setState(() {
|
setState(() {
|
||||||
var temp = values[fieldKey]
|
var temp =
|
||||||
|
values[fieldKey]
|
||||||
as Map<String, MapEntry<int, bool>>;
|
as Map<String, MapEntry<int, bool>>;
|
||||||
temp.removeWhere((key, value) => value.value);
|
temp.removeWhere((key, value) => value.value);
|
||||||
values[fieldKey] = temp;
|
values[fieldKey] = temp;
|
||||||
@ -577,7 +637,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
.deleteConfirmationMessage !=
|
.deleteConfirmationMessage !=
|
||||||
null) {
|
null) {
|
||||||
var message =
|
var message =
|
||||||
(widget.items[r][e] as GeneratedFormTagInput)
|
(widget.items[r][e]
|
||||||
|
as GeneratedFormTagInput)
|
||||||
.deleteConfirmationMessage!;
|
.deleteConfirmationMessage!;
|
||||||
showDialog<Map<String, dynamic>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -585,8 +646,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: message.key,
|
title: message.key,
|
||||||
message: message.value,
|
message: message.value,
|
||||||
items: const []);
|
items: const [],
|
||||||
}).then((value) {
|
);
|
||||||
|
},
|
||||||
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
fn();
|
fn();
|
||||||
}
|
}
|
||||||
@ -598,7 +661,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
icon: const Icon(Icons.remove),
|
icon: const Icon(Icons.remove),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: tr('remove'),
|
tooltip: tr('remove'),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
|
||||||
?.isEmpty ==
|
?.isEmpty ==
|
||||||
@ -610,8 +674,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(
|
label: Text(
|
||||||
(widget.items[r][e] as GeneratedFormTagInput)
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
.label),
|
.label,
|
||||||
))
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@ -619,29 +685,30 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: tr('add'),
|
tooltip: tr('add'),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
} else if (widget.items[r][e] is GeneratedFormSubForm) {
|
||||||
List<Widget> subformColumn = [];
|
List<Widget> subformColumn = [];
|
||||||
var compact = (widget.items[r][e] as GeneratedFormSubForm)
|
var compact =
|
||||||
.items
|
(widget.items[r][e] as GeneratedFormSubForm).items.length == 1 &&
|
||||||
.length ==
|
|
||||||
1 &&
|
|
||||||
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
(widget.items[r][e] as GeneratedFormSubForm).items[0].length == 1;
|
||||||
for (int i = 0; i < values[fieldKey].length; i++) {
|
for (int i = 0; i < values[fieldKey].length; i++) {
|
||||||
var internalFormKey = ValueKey(generateRandomNumber(
|
var internalFormKey = ValueKey(
|
||||||
|
generateRandomNumber(
|
||||||
values[fieldKey].length,
|
values[fieldKey].length,
|
||||||
seed2: i,
|
seed2: i,
|
||||||
seed3: forceUpdateKeyCount));
|
seed3: forceUpdateKeyCount,
|
||||||
subformColumn.add(Column(
|
),
|
||||||
|
);
|
||||||
|
subformColumn.add(
|
||||||
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (!compact)
|
if (!compact) const SizedBox(height: 16),
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
if (!compact)
|
if (!compact)
|
||||||
Text(
|
Text(
|
||||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||||
@ -649,22 +716,30 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
),
|
),
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: internalFormKey,
|
key: internalFormKey,
|
||||||
items: cloneFormItems(
|
items:
|
||||||
(widget.items[r][e] as GeneratedFormSubForm).items)
|
cloneFormItems(
|
||||||
.map((x) => x.map((y) {
|
(widget.items[r][e] as GeneratedFormSubForm)
|
||||||
|
.items,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(x) => x.map((y) {
|
||||||
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
y.defaultValue = values[fieldKey]?[i]?[y.key];
|
||||||
y.key = '${y.key.toString()},$internalFormKey';
|
y.key = '${y.key.toString()},$internalFormKey';
|
||||||
return y;
|
return y;
|
||||||
}).toList())
|
}).toList(),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
values = values.map(
|
values = values.map(
|
||||||
(key, value) => MapEntry(key.split(',')[0], value));
|
(key, value) => MapEntry(key.split(',')[0], value),
|
||||||
|
);
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.values[fieldKey]?[i] = values;
|
this.values[fieldKey]?[i] = values;
|
||||||
}
|
}
|
||||||
someValueChanged(
|
someValueChanged(
|
||||||
isBuilding: isBuilding, forceInvalid: !valid);
|
isBuilding: isBuilding,
|
||||||
|
forceInvalid: !valid,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -672,8 +747,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).colorScheme.error,
|
||||||
Theme.of(context).colorScheme.error),
|
),
|
||||||
onPressed: (values[fieldKey].length > 0)
|
onPressed: (values[fieldKey].length > 0)
|
||||||
? () {
|
? () {
|
||||||
var temp = List.from(values[fieldKey]);
|
var temp = List.from(values[fieldKey]);
|
||||||
@ -686,33 +761,40 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
label: Text(
|
label: Text(
|
||||||
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
'${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
|
||||||
),
|
),
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.delete_outline_rounded),
|
||||||
Icons.delete_outline_rounded,
|
),
|
||||||
))
|
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
subformColumn.add(Padding(
|
subformColumn.add(
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
padding: const EdgeInsets.only(bottom: 0, top: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
values[fieldKey].add(getDefaultValuesFromFormItems(
|
values[fieldKey].add(
|
||||||
(widget.items[r][e] as GeneratedFormSubForm)
|
getDefaultValuesFromFormItems(
|
||||||
.items));
|
(widget.items[r][e] as GeneratedFormSubForm).items,
|
||||||
|
),
|
||||||
|
);
|
||||||
forceUpdateKeyCount++;
|
forceUpdateKeyCount++;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text((widget.items[r][e] as GeneratedFormSubForm)
|
label: Text(
|
||||||
.label))),
|
(widget.items[r][e] as GeneratedFormSubForm).label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
formInputs[r][e] = Column(children: subformColumn);
|
formInputs[r][e] = Column(children: subformColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -726,24 +808,26 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch
|
height: widget.items[rowInputs.key - 1][0] is GeneratedFormSwitch
|
||||||
? 8
|
? 8
|
||||||
: 25,
|
: 25,
|
||||||
)
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
List<Widget> rowItems = [];
|
List<Widget> rowItems = [];
|
||||||
rowInputs.value.asMap().entries.forEach((rowInput) {
|
rowInputs.value.asMap().entries.forEach((rowInput) {
|
||||||
if (rowInput.key > 0) {
|
if (rowInput.key > 0) {
|
||||||
rowItems.add(const SizedBox(
|
rowItems.add(const SizedBox(width: 20));
|
||||||
width: 20,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
rowItems.add(Expanded(
|
rowItems.add(
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
rowInput.value,
|
rowInput.value,
|
||||||
...widget.items[rowInputs.key][rowInput.key].belowWidgets
|
...widget.items[rowInputs.key][rowInput.key].belowWidgets,
|
||||||
])));
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
rows.add(rowItems);
|
rows.add(rowItems);
|
||||||
});
|
});
|
||||||
@ -752,12 +836,15 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
...rows.map((row) => Row(
|
...rows.map(
|
||||||
|
(row) => Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [...row.map((e) => e)],
|
children: [...row.map((e) => e)],
|
||||||
))
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,16 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
|
||||||
class GeneratedFormModal extends StatefulWidget {
|
class GeneratedFormModal extends StatefulWidget {
|
||||||
const GeneratedFormModal(
|
const GeneratedFormModal({
|
||||||
{super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
this.initValid = false,
|
this.initValid = false,
|
||||||
this.message = '',
|
this.message = '',
|
||||||
this.additionalWidgets = const [],
|
this.additionalWidgets = const [],
|
||||||
this.singleNullReturnButton,
|
this.singleNullReturnButton,
|
||||||
this.primaryActionColour});
|
this.primaryActionColour,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
@ -41,13 +42,11 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
content:
|
content: Column(
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
if (widget.message.isNotEmpty) Text(widget.message),
|
if (widget.message.isNotEmpty) Text(widget.message),
|
||||||
if (widget.message.isNotEmpty)
|
if (widget.message.isNotEmpty) const SizedBox(height: 16),
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
items: widget.items,
|
items: widget.items,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
@ -60,23 +59,29 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
this.valid = valid;
|
this.valid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets
|
),
|
||||||
]),
|
if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets,
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(widget.singleNullReturnButton == null
|
child: Text(
|
||||||
|
widget.singleNullReturnButton == null
|
||||||
? tr('cancel')
|
? tr('cancel')
|
||||||
: widget.singleNullReturnButton!)),
|
: widget.singleNullReturnButton!,
|
||||||
|
),
|
||||||
|
),
|
||||||
widget.singleNullReturnButton == null
|
widget.singleNullReturnButton == null
|
||||||
? TextButton(
|
? TextButton(
|
||||||
style: widget.primaryActionColour == null
|
style: widget.primaryActionColour == null
|
||||||
? null
|
? null
|
||||||
: TextButton.styleFrom(
|
: TextButton.styleFrom(
|
||||||
foregroundColor: widget.primaryActionColour),
|
foregroundColor: widget.primaryActionColour,
|
||||||
|
),
|
||||||
onPressed: !valid
|
onPressed: !valid
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@ -85,8 +90,9 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
Navigator.of(context).pop(values);
|
Navigator.of(context).pop(values);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(tr('continue')))
|
child: Text(tr('continue')),
|
||||||
: const SizedBox.shrink()
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,8 @@ class CredsNeededError extends ObtainiumError {
|
|||||||
class NoReleasesError extends ObtainiumError {
|
class NoReleasesError extends ObtainiumError {
|
||||||
NoReleasesError({String? note})
|
NoReleasesError({String? note})
|
||||||
: super(
|
: super(
|
||||||
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
|
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoAPKError extends ObtainiumError {
|
class NoAPKError extends ObtainiumError {
|
||||||
@ -75,7 +76,7 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
|
|
||||||
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
MultiAppMultiError() : super(tr('placeholder'), unexpected: true);
|
||||||
|
|
||||||
add(String appId, dynamic error, {String? appName}) {
|
void add(String appId, dynamic error, {String? appName}) {
|
||||||
if (error is SocketException) {
|
if (error is SocketException) {
|
||||||
error = error.message;
|
error = error.message;
|
||||||
}
|
}
|
||||||
@ -93,8 +94,11 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
String errorString(String appId, {bool includeIdsWithNames = false}) =>
|
||||||
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
'${appIdNames.containsKey(appId) ? '${appIdNames[appId]}${includeIdsWithNames ? ' ($appId)' : ''}' : appId}: ${rawErrors[appId].toString()}';
|
||||||
|
|
||||||
String errorsAppsString(String errString, List<String> appIds,
|
String errorsAppsString(
|
||||||
{bool includeIdsWithNames = false}) =>
|
String errString,
|
||||||
|
List<String> appIds, {
|
||||||
|
bool includeIdsWithNames = false,
|
||||||
|
}) =>
|
||||||
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
'$errString [${list2FriendlyString(appIds.map((id) => appIdNames.containsKey(id) == true ? '${appIdNames[id]}${includeIdsWithNames ? ' ($id)' : ''}' : id).toList())}]';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -103,43 +107,50 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage(dynamic e, BuildContext context, {bool isError = false}) {
|
void showMessage(dynamic e, BuildContext context, {bool isError = false}) {
|
||||||
Provider.of<LogsProvider>(context, listen: false)
|
Provider.of<LogsProvider>(
|
||||||
.add(e.toString(), level: isError ? LogLevels.error : LogLevels.info);
|
context,
|
||||||
|
listen: false,
|
||||||
|
).add(e.toString(), level: isError ? LogLevels.error : LogLevels.info);
|
||||||
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(e.toString())),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
} else {
|
} else {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(e is MultiAppMultiError
|
title: Text(
|
||||||
|
e is MultiAppMultiError
|
||||||
? tr(isError ? 'someErrors' : 'updates')
|
? tr(isError ? 'someErrors' : 'updates')
|
||||||
: tr(isError ? 'unexpectedError' : 'unknown')),
|
: tr(isError ? 'unexpectedError' : 'unknown'),
|
||||||
|
),
|
||||||
content: GestureDetector(
|
content: GestureDetector(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
Clipboard.setData(ClipboardData(text: e.toString()));
|
Clipboard.setData(ClipboardData(text: e.toString()));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(
|
||||||
content: Text(tr('copiedToClipboard')),
|
context,
|
||||||
));
|
).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard'))));
|
||||||
},
|
},
|
||||||
child: Text(e.toString())),
|
child: Text(e.toString()),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(tr('ok'))),
|
child: Text(tr('ok')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showError(dynamic e, BuildContext context) {
|
void showError(dynamic e, BuildContext context) {
|
||||||
showMessage(e, context, isError: true);
|
showMessage(e, context, isError: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,12 +160,14 @@ String list2FriendlyString(List<String> list) {
|
|||||||
: list
|
: list
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) =>
|
.map(
|
||||||
|
(e) =>
|
||||||
e.value +
|
e.value +
|
||||||
(e.key == list.length - 1
|
(e.key == list.length - 1
|
||||||
? ''
|
? ''
|
||||||
: e.key == list.length - 2
|
: e.key == list.length - 2
|
||||||
? ' and '
|
? ' and '
|
||||||
: ', '))
|
: ', '),
|
||||||
|
)
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,10 @@ List<MapEntry<Locale, String>> supportedLocales = const [
|
|||||||
MapEntry(Locale('tr'), 'Türkçe'),
|
MapEntry(Locale('tr'), 'Türkçe'),
|
||||||
MapEntry(Locale('uk'), 'Українська'),
|
MapEntry(Locale('uk'), 'Українська'),
|
||||||
MapEntry(Locale('da'), 'Dansk'),
|
MapEntry(Locale('da'), 'Dansk'),
|
||||||
MapEntry(Locale('en', 'EO'),
|
MapEntry(
|
||||||
'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
Locale('en', 'EO'),
|
||||||
|
'Esperanto',
|
||||||
|
), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
||||||
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
||||||
MapEntry(Locale('ko'), '한국어'),
|
MapEntry(Locale('ko'), '한국어'),
|
||||||
MapEntry(Locale('ca'), 'Català'),
|
MapEntry(Locale('ca'), 'Català'),
|
||||||
@ -76,9 +78,11 @@ Future<void> loadTranslations() async {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await controller.loadTranslations();
|
await controller.loadTranslations();
|
||||||
Localization.load(controller.locale,
|
Localization.load(
|
||||||
|
controller.locale,
|
||||||
translations: controller.translations,
|
translations: controller.translations,
|
||||||
fallbackTranslations: controller.fallbackTranslations);
|
fallbackTranslations: controller.fallbackTranslations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
@ -97,10 +101,12 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
try {
|
try {
|
||||||
ByteData data =
|
ByteData data = await PlatformAssetBundle().load(
|
||||||
await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
|
'assets/ca/lets-encrypt-r3.pem',
|
||||||
SecurityContext.defaultContext
|
);
|
||||||
.setTrustedCertificatesBytes(data.buffer.asUint8List());
|
SecurityContext.defaultContext.setTrustedCertificatesBytes(
|
||||||
|
data.buffer.asUint8List(),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Already added, do nothing (see #375)
|
// Already added, do nothing (see #375)
|
||||||
}
|
}
|
||||||
@ -113,20 +119,23 @@ void main() async {
|
|||||||
}
|
}
|
||||||
final np = NotificationsProvider();
|
final np = NotificationsProvider();
|
||||||
await np.initialize();
|
await np.initialize();
|
||||||
runApp(MultiProvider(
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
||||||
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
||||||
Provider(create: (context) => np),
|
Provider(create: (context) => np),
|
||||||
Provider(create: (context) => LogsProvider())
|
Provider(create: (context) => LogsProvider()),
|
||||||
],
|
],
|
||||||
child: EasyLocalization(
|
child: EasyLocalization(
|
||||||
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
useOnlyLangCode: false,
|
useOnlyLangCode: false,
|
||||||
child: const Obtainium()),
|
child: const Obtainium(),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,13 +166,17 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
requiresCharging: false,
|
requiresCharging: false,
|
||||||
requiresStorageNotLow: false,
|
requiresStorageNotLow: false,
|
||||||
requiresDeviceIdle: false,
|
requiresDeviceIdle: false,
|
||||||
requiredNetworkType: NetworkType.ANY), (String taskId) async {
|
requiredNetworkType: NetworkType.ANY,
|
||||||
|
),
|
||||||
|
(String taskId) async {
|
||||||
await bgUpdateCheck(taskId, null);
|
await bgUpdateCheck(taskId, null);
|
||||||
BackgroundFetch.finish(taskId);
|
BackgroundFetch.finish(taskId);
|
||||||
}, (String taskId) async {
|
},
|
||||||
|
(String taskId) async {
|
||||||
context.read<LogsProvider>().add('BG update task timed out.');
|
context.read<LogsProvider>().add('BG update task timed out.');
|
||||||
BackgroundFetch.finish(taskId);
|
BackgroundFetch.finish(taskId);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +196,8 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||||
Permission.notification.request();
|
Permission.notification.request();
|
||||||
if (!fdroid) {
|
if (!fdroid) {
|
||||||
getInstalledInfo(obtainiumId).then((value) {
|
getInstalledInfo(obtainiumId)
|
||||||
|
.then((value) {
|
||||||
if (value?.versionName != null) {
|
if (value?.versionName != null) {
|
||||||
appsProvider.saveApps([
|
appsProvider.saveApps([
|
||||||
App(
|
App(
|
||||||
@ -198,13 +212,15 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
{
|
{
|
||||||
'versionDetection': true,
|
'versionDetection': true,
|
||||||
'apkFilterRegEx': 'fdroid',
|
'apkFilterRegEx': 'fdroid',
|
||||||
'invertAPKFilter': true
|
'invertAPKFilter': true,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
false)
|
false,
|
||||||
|
),
|
||||||
], onlyIfExists: false);
|
], onlyIfExists: false);
|
||||||
}
|
}
|
||||||
}).catchError((err) {
|
})
|
||||||
|
.catchError((err) {
|
||||||
print(err);
|
print(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -231,17 +247,20 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
lightColorScheme = lightDynamic.harmonized();
|
lightColorScheme = lightDynamic.harmonized();
|
||||||
darkColorScheme = darkDynamic.harmonized();
|
darkColorScheme = darkDynamic.harmonized();
|
||||||
} else {
|
} else {
|
||||||
lightColorScheme =
|
lightColorScheme = ColorScheme.fromSeed(
|
||||||
ColorScheme.fromSeed(seedColor: settingsProvider.themeColor);
|
seedColor: settingsProvider.themeColor,
|
||||||
|
);
|
||||||
darkColorScheme = ColorScheme.fromSeed(
|
darkColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: settingsProvider.themeColor,
|
seedColor: settingsProvider.themeColor,
|
||||||
brightness: Brightness.dark);
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the background and surface colors to pure black in the amoled theme
|
// set the background and surface colors to pure black in the amoled theme
|
||||||
if (settingsProvider.useBlackTheme) {
|
if (settingsProvider.useBlackTheme) {
|
||||||
darkColorScheme =
|
darkColorScheme = darkColorScheme
|
||||||
darkColorScheme.copyWith(surface: Colors.black).harmonized();
|
.copyWith(surface: Colors.black)
|
||||||
|
.harmonized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
if (settingsProvider.useSystemFont) NativeFeatures.loadSystemFont();
|
||||||
@ -258,18 +277,27 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
colorScheme: settingsProvider.theme == ThemeSettings.dark
|
||||||
? darkColorScheme
|
? darkColorScheme
|
||||||
: lightColorScheme,
|
: lightColorScheme,
|
||||||
fontFamily:
|
fontFamily: settingsProvider.useSystemFont
|
||||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'),
|
? 'SystemFont'
|
||||||
|
: 'Montserrat',
|
||||||
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: settingsProvider.theme == ThemeSettings.light
|
colorScheme: settingsProvider.theme == ThemeSettings.light
|
||||||
? lightColorScheme
|
? lightColorScheme
|
||||||
: darkColorScheme,
|
: darkColorScheme,
|
||||||
fontFamily:
|
fontFamily: settingsProvider.useSystemFont
|
||||||
settingsProvider.useSystemFont ? 'SystemFont' : 'Montserrat'),
|
? 'SystemFont'
|
||||||
home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
|
: 'Montserrat',
|
||||||
|
),
|
||||||
|
home: Shortcuts(
|
||||||
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
}, child: const HomePage()));
|
},
|
||||||
});
|
child: const HomePage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,15 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
late List<String> requiredArgs = [tr('uname')];
|
late List<String> requiredArgs = [tr('uname')];
|
||||||
|
|
||||||
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username,
|
||||||
|
int page,
|
||||||
|
) async {
|
||||||
Response res = await get(
|
Response res = await get(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
'https://api.github.com/users/$username/starred?per_page=100&page=$page',
|
||||||
headers: await GitHub().getRequestHeaders({}));
|
),
|
||||||
|
headers: await GitHub().getRequestHeaders({}),
|
||||||
|
);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
@ -27,8 +31,8 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
e['full_name'] as String,
|
e['full_name'] as String,
|
||||||
e['description'] != null
|
e['description'] != null
|
||||||
? e['description'] as String
|
? e['description'] as String
|
||||||
: tr('noDescription')
|
: tr('noDescription'),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
@ -41,15 +45,18 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
||||||
List<String> args) async {
|
List<String> args,
|
||||||
|
) async {
|
||||||
if (args.length != requiredArgs.length) {
|
if (args.length != requiredArgs.length) {
|
||||||
throw ObtainiumError(tr('wrongArgNum'));
|
throw ObtainiumError(tr('wrongArgNum'));
|
||||||
}
|
}
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
var page = 1;
|
var page = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pageUrls =
|
var pageUrls = await getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
await getOnePageOfUserStarredUrlsWithDescriptions(args[0], page++);
|
args[0],
|
||||||
|
page++,
|
||||||
|
);
|
||||||
urlsWithDescriptions.addAll(pageUrls);
|
urlsWithDescriptions.addAll(pageUrls);
|
||||||
if (pageUrls.length < 100) {
|
if (pageUrls.length < 100) {
|
||||||
break;
|
break;
|
||||||
|
@ -39,7 +39,7 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
int urlInputKey = 0;
|
int urlInputKey = 0;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
|
|
||||||
linkFn(String input) {
|
void linkFn(String input) {
|
||||||
try {
|
try {
|
||||||
if (input.isEmpty) {
|
if (input.isEmpty) {
|
||||||
throw UnsupportedURLError();
|
throw UnsupportedURLError();
|
||||||
@ -51,8 +51,13 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeUserInput(String input, bool valid, bool isBuilding,
|
void changeUserInput(
|
||||||
{bool updateUrlInput = false, String? overrideSource}) {
|
String input,
|
||||||
|
bool valid,
|
||||||
|
bool isBuilding, {
|
||||||
|
bool updateUrlInput = false,
|
||||||
|
String? overrideSource,
|
||||||
|
}) {
|
||||||
userInput = input;
|
userInput = input;
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -69,8 +74,10 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
? pickedSource?.hosts[0]
|
? pickedSource?.hosts[0]
|
||||||
: null;
|
: null;
|
||||||
var source = valid
|
var source = valid
|
||||||
? sourceProvider.getSource(userInput,
|
? sourceProvider.getSource(
|
||||||
overrideSource: pickedSourceOverride)
|
userInput,
|
||||||
|
overrideSource: pickedSourceOverride,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
if (pickedSource.runtimeType != source.runtimeType ||
|
if (pickedSource.runtimeType != source.runtimeType ||
|
||||||
overrideChanged ||
|
overrideChanged ||
|
||||||
@ -79,7 +86,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
pickedSource?.runOnAddAppInputChange(userInput);
|
pickedSource?.runOnAddAppInputChange(userInput);
|
||||||
additionalSettings = source != null
|
additionalSettings = source != null
|
||||||
? getDefaultValuesFromFormItems(
|
? getDefaultValuesFromFormItems(
|
||||||
source.combinedAppSpecificSettingFormItems)
|
source.combinedAppSpecificSettingFormItems,
|
||||||
|
)
|
||||||
: {};
|
: {};
|
||||||
additionalSettingsValid = source != null
|
additionalSettingsValid = source != null
|
||||||
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
||||||
@ -94,13 +102,15 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
NotificationsProvider notificationsProvider =
|
NotificationsProvider notificationsProvider = context
|
||||||
context.read<NotificationsProvider>();
|
.read<NotificationsProvider>();
|
||||||
|
|
||||||
bool doingSomething = gettingAppInfo || searching;
|
bool doingSomething = gettingAppInfo || searching;
|
||||||
|
|
||||||
Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
|
Future<bool> getTrackOnlyConfirmationIfNeeded(
|
||||||
{bool ignoreHideSetting = false}) async {
|
bool userPickedTrackOnly, {
|
||||||
|
bool ignoreHideSetting = false,
|
||||||
|
}) async {
|
||||||
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
|
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
|
||||||
if (useTrackOnly &&
|
if (useTrackOnly &&
|
||||||
(!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
|
(!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
|
||||||
@ -110,16 +120,20 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
initValid: true,
|
initValid: true,
|
||||||
title: tr('xIsTrackOnly', args: [
|
title: tr(
|
||||||
pickedSource!.enforceTrackOnly ? tr('source') : tr('app')
|
'xIsTrackOnly',
|
||||||
]),
|
args: [
|
||||||
|
pickedSource!.enforceTrackOnly ? tr('source') : tr('app'),
|
||||||
|
],
|
||||||
|
),
|
||||||
items: [
|
items: [
|
||||||
[GeneratedFormSwitch('hide', label: tr('dontShowAgain'))]
|
[GeneratedFormSwitch('hide', label: tr('dontShowAgain'))],
|
||||||
],
|
],
|
||||||
message:
|
message:
|
||||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
|
settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
|
||||||
}
|
}
|
||||||
@ -130,7 +144,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getReleaseDateAsVersionConfirmationIfNeeded(
|
getReleaseDateAsVersionConfirmationIfNeeded(
|
||||||
bool userPickedTrackOnly) async {
|
bool userPickedTrackOnly,
|
||||||
|
) async {
|
||||||
return (!(additionalSettings['releaseDateAsVersion'] == true &&
|
return (!(additionalSettings['releaseDateAsVersion'] == true &&
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@ -141,7 +156,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
items: const [],
|
items: const [],
|
||||||
message: tr('releaseDateAsVersionExplanation'),
|
message: tr('releaseDateAsVersionExplanation'),
|
||||||
);
|
);
|
||||||
}) ==
|
},
|
||||||
|
) ==
|
||||||
null));
|
null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,27 +170,38 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
App? app;
|
App? app;
|
||||||
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
||||||
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
||||||
userPickedTrackOnly))) {
|
userPickedTrackOnly,
|
||||||
|
))) {
|
||||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||||
app = await sourceProvider.getApp(
|
app = await sourceProvider.getApp(
|
||||||
pickedSource!, userInput.trim(), additionalSettings,
|
pickedSource!,
|
||||||
|
userInput.trim(),
|
||||||
|
additionalSettings,
|
||||||
trackOnlyOverride: trackOnly,
|
trackOnlyOverride: trackOnly,
|
||||||
sourceIsOverriden: pickedSourceOverride != null,
|
sourceIsOverriden: pickedSourceOverride != null,
|
||||||
inferAppIdIfOptional: inferAppIdIfOptional);
|
inferAppIdIfOptional: inferAppIdIfOptional,
|
||||||
|
);
|
||||||
// Only download the APK here if you need to for the package ID
|
// Only download the APK here if you need to for the package ID
|
||||||
if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
|
if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var apkUrl =
|
var apkUrl = await appsProvider.confirmAppFileUrl(
|
||||||
await appsProvider.confirmAppFileUrl(app, context, false);
|
app,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
);
|
||||||
if (apkUrl == null) {
|
if (apkUrl == null) {
|
||||||
throw ObtainiumError(tr('cancelled'));
|
throw ObtainiumError(tr('cancelled'));
|
||||||
}
|
}
|
||||||
app.preferredApkIndex =
|
app.preferredApkIndex = app.apkUrls
|
||||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
.map((e) => e.value)
|
||||||
|
.toList()
|
||||||
|
.indexOf(apkUrl.value);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedArtifact = await appsProvider.downloadApp(
|
var downloadedArtifact = await appsProvider.downloadApp(
|
||||||
app, globalNavigatorKey.currentContext,
|
app,
|
||||||
notificationsProvider: notificationsProvider);
|
globalNavigatorKey.currentContext,
|
||||||
|
notificationsProvider: notificationsProvider,
|
||||||
|
);
|
||||||
DownloadedApk? downloadedFile;
|
DownloadedApk? downloadedFile;
|
||||||
DownloadedXApkDir? downloadedDir;
|
DownloadedXApkDir? downloadedDir;
|
||||||
if (downloadedArtifact is DownloadedApk) {
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
@ -195,8 +222,10 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
await appsProvider.saveApps([app], onlyIfExists: false);
|
await appsProvider.saveApps([app], onlyIfExists: false);
|
||||||
}
|
}
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
Navigator.push(globalNavigatorKey.currentContext ?? context,
|
Navigator.push(
|
||||||
MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)));
|
globalNavigatorKey.currentContext ?? context,
|
||||||
|
MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
@ -217,15 +246,18 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
key: Key(urlInputKey.toString()),
|
key: Key(urlInputKey.toString()),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appSourceURL',
|
GeneratedFormTextField(
|
||||||
|
'appSourceURL',
|
||||||
label: tr('appSourceURL'),
|
label: tr('appSourceURL'),
|
||||||
defaultValue: userInput,
|
defaultValue: userInput,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
try {
|
try {
|
||||||
sourceProvider
|
sourceProvider
|
||||||
.getSource(value ?? '',
|
.getSource(
|
||||||
overrideSource: pickedSourceOverride)
|
value ?? '',
|
||||||
|
overrideSource: pickedSourceOverride,
|
||||||
|
)
|
||||||
.standardizeUrl(value ?? '');
|
.standardizeUrl(value ?? '');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e is String
|
return e is String
|
||||||
@ -235,23 +267,25 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
: tr('error');
|
: tr('error');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
changeUserInput(
|
changeUserInput(values['appSourceURL']!, valid, isBuilding);
|
||||||
values['appSourceURL']!, valid, isBuilding);
|
},
|
||||||
})),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
gettingAppInfo
|
gettingAppInfo
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: doingSomething ||
|
onPressed:
|
||||||
|
doingSomething ||
|
||||||
pickedSource == null ||
|
pickedSource == null ||
|
||||||
(pickedSource!.combinedAppSpecificSettingFormItems
|
(pickedSource!
|
||||||
|
.combinedAppSpecificSettingFormItems
|
||||||
.isNotEmpty &&
|
.isNotEmpty &&
|
||||||
!additionalSettingsValid)
|
!additionalSettingsValid)
|
||||||
? null
|
? null
|
||||||
@ -259,7 +293,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
addApp();
|
addApp();
|
||||||
},
|
},
|
||||||
child: Text(tr('add')))
|
child: Text(tr('add')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -272,7 +307,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
sourceStrings[s.name] = [s.name];
|
sourceStrings[s.name] = [s.name];
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
var searchSources = await showDialog<List<String>?>(
|
var searchSources =
|
||||||
|
await showDialog<List<String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return SelectionModal(
|
return SelectionModal(
|
||||||
@ -283,14 +319,16 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
titlesAreLinks: false,
|
titlesAreLinks: false,
|
||||||
deselectThese: settingsProvider.searchDeselected,
|
deselectThese: settingsProvider.searchDeselected,
|
||||||
);
|
);
|
||||||
}) ??
|
},
|
||||||
|
) ??
|
||||||
[];
|
[];
|
||||||
if (searchSources.isNotEmpty) {
|
if (searchSources.isNotEmpty) {
|
||||||
settingsProvider.searchDeselected = sourceStrings.keys
|
settingsProvider.searchDeselected = sourceStrings.keys
|
||||||
.where((s) => !searchSources.contains(s))
|
.where((s) => !searchSources.contains(s))
|
||||||
.toList();
|
.toList();
|
||||||
List<MapEntry<String, Map<String, List<String>>>?> results =
|
List<MapEntry<String, Map<String, List<String>>>?>
|
||||||
(await Future.wait(sourceProvider.sources
|
results = (await Future.wait(
|
||||||
|
sourceProvider.sources
|
||||||
.where((e) => searchSources.contains(e.name))
|
.where((e) => searchSources.contains(e.name))
|
||||||
.map((e) async {
|
.map((e) async {
|
||||||
try {
|
try {
|
||||||
@ -304,38 +342,48 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
items: [
|
items: [
|
||||||
...e.searchQuerySettingFormItems.map((e) => [e]),
|
...e.searchQuerySettingFormItems.map((e) => [e]),
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('url',
|
GeneratedFormTextField(
|
||||||
|
'url',
|
||||||
label: e.hosts.isNotEmpty
|
label: e.hosts.isNotEmpty
|
||||||
? tr('overrideSource')
|
? tr('overrideSource')
|
||||||
: plural('url', 1).substring(2),
|
: plural('url', 1).substring(2),
|
||||||
autoCompleteOptions: [
|
autoCompleteOptions: [
|
||||||
...(e.hosts.isNotEmpty ? [e.hosts[0]] : []),
|
...(e.hosts.isNotEmpty ? [e.hosts[0]] : []),
|
||||||
...appsProvider.apps.values
|
...appsProvider.apps.values
|
||||||
.where((a) =>
|
.where(
|
||||||
|
(a) =>
|
||||||
sourceProvider
|
sourceProvider
|
||||||
.getSource(a.app.url,
|
.getSource(
|
||||||
|
a.app.url,
|
||||||
overrideSource:
|
overrideSource:
|
||||||
a.app.overrideSource)
|
a.app.overrideSource,
|
||||||
|
)
|
||||||
.runtimeType ==
|
.runtimeType ==
|
||||||
e.runtimeType)
|
e.runtimeType,
|
||||||
|
)
|
||||||
.map((a) {
|
.map((a) {
|
||||||
var uri = Uri.parse(a.app.url);
|
var uri = Uri.parse(a.app.url);
|
||||||
return '${uri.origin}${uri.path}';
|
return '${uri.origin}${uri.path}';
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
defaultValue:
|
defaultValue: e.hosts.isNotEmpty
|
||||||
e.hosts.isNotEmpty ? e.hosts[0] : '',
|
? e.hosts[0]
|
||||||
required: true)
|
: '',
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (querySettings == null) {
|
if (querySettings == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MapEntry(e.runtimeType.toString(),
|
return MapEntry(
|
||||||
await e.search(searchQuery, querySettings: querySettings));
|
e.runtimeType.toString(),
|
||||||
|
await e.search(searchQuery, querySettings: querySettings),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err is! CredsNeededError) {
|
if (err is! CredsNeededError) {
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -345,9 +393,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})))
|
}),
|
||||||
.where((a) => a != null)
|
)).where((a) => a != null).toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Interleave results instead of simple reduce
|
// Interleave results instead of simple reduce
|
||||||
Map<String, MapEntry<String, List<String>>> res = {};
|
Map<String, MapEntry<String, List<String>>> res = {};
|
||||||
@ -379,11 +426,17 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
selectedByDefault: false,
|
selectedByDefault: false,
|
||||||
onlyOneSelectionAllowed: true,
|
onlyOneSelectionAllowed: true,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||||
var sourceName = res[selectedUrls[0]]?.key;
|
var sourceName = res[selectedUrls[0]]?.key;
|
||||||
changeUserInput(selectedUrls[0], true, false,
|
changeUserInput(
|
||||||
updateUrlInput: true, overrideSource: sourceName);
|
selectedUrls[0],
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
updateUrlInput: true,
|
||||||
|
overrideSource: sourceName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -395,7 +448,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getHTMLSourceOverrideDropdown() => Column(children: [
|
Widget getHTMLSourceOverrideDropdown() => Column(
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -408,20 +462,25 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
[
|
[
|
||||||
MapEntry('', tr('none')),
|
MapEntry('', tr('none')),
|
||||||
...sourceProvider.sources
|
...sourceProvider.sources
|
||||||
.where((s) =>
|
.where(
|
||||||
|
(s) =>
|
||||||
s.allowOverride ||
|
s.allowOverride ||
|
||||||
(pickedSource != null &&
|
(pickedSource != null &&
|
||||||
pickedSource.runtimeType ==
|
pickedSource.runtimeType ==
|
||||||
s.runtimeType))
|
s.runtimeType),
|
||||||
.map((s) =>
|
)
|
||||||
MapEntry(s.runtimeType.toString(), s.name))
|
.map(
|
||||||
|
(s) => MapEntry(s.runtimeType.toString(), s.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
label: tr('overrideSource'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
label: tr('overrideSource'))
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
fn() {
|
fn() {
|
||||||
pickedSourceOverride = (values['overrideSource'] == null ||
|
pickedSourceOverride =
|
||||||
|
(values['overrideSource'] == null ||
|
||||||
values['overrideSource'] == '')
|
values['overrideSource'] == '')
|
||||||
? null
|
? null
|
||||||
: values['overrideSource'];
|
: values['overrideSource'];
|
||||||
@ -436,13 +495,13 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
changeUserInput(userInput, valid, isBuilding);
|
changeUserInput(userInput, valid, isBuilding);
|
||||||
},
|
},
|
||||||
))
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
],
|
||||||
)
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
bool shouldShowSearchBar() =>
|
bool shouldShowSearchBar() =>
|
||||||
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
||||||
@ -455,9 +514,12 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
child: GeneratedForm(
|
child: GeneratedForm(
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('searchSomeSources',
|
GeneratedFormTextField(
|
||||||
label: tr('searchSomeSourcesLabel'), required: false),
|
'searchSomeSources',
|
||||||
]
|
label: tr('searchSomeSourcesLabel'),
|
||||||
|
required: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (values.isNotEmpty && valid && !isBuilding) {
|
if (values.isNotEmpty && valid && !isBuilding) {
|
||||||
@ -465,11 +527,10 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
searchQuery = values['searchSomeSources']!.trim();
|
searchQuery = values['searchSomeSources']!.trim();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
searching
|
searching
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
@ -478,34 +539,32 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
: () {
|
: () {
|
||||||
runSearch();
|
runSearch();
|
||||||
},
|
},
|
||||||
child: Text(tr('search')))
|
child: Text(tr('search')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget getAdditionalOptsCol() => Column(
|
Widget getAdditionalOptsCol() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
tr('additionalOptsFor',
|
tr('additionalOptsFor', args: [pickedSource?.name ?? tr('source')]),
|
||||||
args: [pickedSource?.name ?? tr('source')]),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold,
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: Key(
|
key: Key(
|
||||||
'${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}'),
|
'${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}',
|
||||||
|
),
|
||||||
items: [
|
items: [
|
||||||
...pickedSource!.combinedAppSpecificSettingFormItems,
|
...pickedSource!.combinedAppSpecificSettingFormItems,
|
||||||
...(pickedSourceOverride != null
|
...(pickedSourceOverride != null
|
||||||
? pickedSource!.sourceConfigSettingFormItems
|
? pickedSource!.sourceConfigSettingFormItems.map((e) => [e])
|
||||||
.map((e) => [e])
|
: []),
|
||||||
: [])
|
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
@ -514,17 +573,17 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalSettingsValid = valid;
|
additionalSettingsValid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
CategoryEditorSelector(
|
CategoryEditorSelector(
|
||||||
alignment: WrapAlignment.start,
|
alignment: WrapAlignment.start,
|
||||||
onSelected: (categories) {
|
onSelected: (categories) {
|
||||||
pickedCategories = categories;
|
pickedCategories = categories;
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null && pickedSource!.appIdInferIsOptional)
|
if (pickedSource != null && pickedSource!.appIdInferIsOptional)
|
||||||
@ -532,10 +591,12 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
key: const Key('inferAppIdIfOptional'),
|
key: const Key('inferAppIdIfOptional'),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('inferAppIdIfOptional',
|
GeneratedFormSwitch(
|
||||||
|
'inferAppIdIfOptional',
|
||||||
label: tr('tryInferAppIdFromCode'),
|
label: tr('tryInferAppIdFromCode'),
|
||||||
defaultValue: inferAppIdIfOptional)
|
defaultValue: inferAppIdIfOptional,
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
@ -543,14 +604,17 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
inferAppIdIfOptional = values['inferAppIdIfOptional'];
|
inferAppIdIfOptional = values['inferAppIdIfOptional'];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
if (pickedSource != null && pickedSource!.enforceTrackOnly)
|
if (pickedSource != null && pickedSource!.enforceTrackOnly)
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: Key(
|
key: Key(
|
||||||
'${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}-appId'),
|
'${pickedSource.runtimeType.toString()}-${pickedSource?.hostChanged.toString()}-${pickedSource?.hostIdenticalDespiteAnyChange.toString()}-appId',
|
||||||
|
),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appId',
|
GeneratedFormTextField(
|
||||||
|
'appId',
|
||||||
label: '${tr('appId')} - ${tr('custom')}',
|
label: '${tr('appId')} - ${tr('custom')}',
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
@ -559,15 +623,16 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final isValid = RegExp(
|
final isValid = RegExp(
|
||||||
r'^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Za-z][A-Za-z\d_]*$')
|
r'^([A-Za-z]{1}[A-Za-z\d_]*\.)+[A-Za-z][A-Za-z\d_]*$',
|
||||||
.hasMatch(value);
|
).hasMatch(value);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return tr('invalidInput');
|
return tr('invalidInput');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
]),
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
@ -575,7 +640,8 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalSettings['appId'] = values['appId'];
|
additionalSettings['appId'] = values['appId'];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -598,15 +664,14 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalWidgets: [
|
additionalWidgets: [
|
||||||
...sourceProvider.sources.map(
|
...sourceProvider.sources.map(
|
||||||
(e) => Padding(
|
(e) => Padding(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: e.hosts.isNotEmpty
|
onTap: e.hosts.isNotEmpty
|
||||||
? () {
|
? () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://${e.hosts[0]}',
|
'https://${e.hosts[0]}',
|
||||||
mode: LaunchMode
|
mode: LaunchMode.externalApplication,
|
||||||
.externalApplication);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -614,22 +679,19 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
decoration: e.hosts.isNotEmpty
|
decoration: e.hosts.isNotEmpty
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none),
|
: TextDecoration.none,
|
||||||
))),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'${tr('note')}:',
|
'${tr('note')}:',
|
||||||
style:
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 4),
|
||||||
height: 4,
|
Text(tr('selfHostedNote', args: [tr('overrideSource')])),
|
||||||
),
|
|
||||||
Text(tr('selfHostedNote',
|
|
||||||
args: [tr('overrideSource')])),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -640,19 +702,24 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontStyle: FontStyle.italic),
|
fontStyle: FontStyle.italic,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString('https://apps.obtainium.imranr.dev/',
|
launchUrlString(
|
||||||
mode: LaunchMode.externalApplication);
|
'https://apps.obtainium.imranr.dev/',
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('crowdsourcedConfigsShort'),
|
tr('crowdsourcedConfigsShort'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontStyle: FontStyle.italic),
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -661,9 +728,10 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
bottomNavigationBar:
|
bottomNavigationBar: pickedSource == null ? getSourcesListWidget() : null,
|
||||||
pickedSource == null ? getSourcesListWidget() : null,
|
body: CustomScrollView(
|
||||||
body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
|
shrinkWrap: true,
|
||||||
|
slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('addApp')),
|
CustomAppBar(title: tr('addApp')),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -673,9 +741,7 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
getUrlInputRow(),
|
getUrlInputRow(),
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
if (pickedSource != null) getHTMLSourceOverrideDropdown(),
|
if (pickedSource != null) getHTMLSourceOverrideDropdown(),
|
||||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||||
if (pickedSource != null)
|
if (pickedSource != null)
|
||||||
@ -684,15 +750,19 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
return val.data != null && val.data!.isNotEmpty
|
return val.data != null && val.data!.isNotEmpty
|
||||||
? Text(
|
? Text(
|
||||||
val.data!,
|
val.data!,
|
||||||
style:
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
Theme.of(context).textTheme.bodySmall,
|
|
||||||
)
|
)
|
||||||
: const SizedBox();
|
: const SizedBox();
|
||||||
},
|
},
|
||||||
future: pickedSource?.getSourceNote()),
|
future: pickedSource?.getSourceNote(),
|
||||||
|
),
|
||||||
if (pickedSource != null) getAdditionalOptsCol(),
|
if (pickedSource != null) getAdditionalOptsCol(),
|
||||||
])),
|
],
|
||||||
)
|
),
|
||||||
]));
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onWebResourceError: (WebResourceError error) {
|
onWebResourceError: (WebResourceError error) {
|
||||||
if (error.isForMainFrame == true) {
|
if (error.isForMainFrame == true) {
|
||||||
showError(
|
showError(
|
||||||
ObtainiumError(error.description, unexpected: true), context);
|
ObtainiumError(error.description, unexpected: true),
|
||||||
|
context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigationRequest: (NavigationRequest request) =>
|
onNavigationRequest: (NavigationRequest request) =>
|
||||||
@ -85,8 +87,10 @@ class _AppPageState extends State<AppPage> {
|
|||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
||||||
var source = app != null
|
var source = app != null
|
||||||
? sourceProvider.getSource(app.app.url,
|
? sourceProvider.getSource(
|
||||||
overrideSource: app.app.overrideSource)
|
app.app.url,
|
||||||
|
overrideSource: app.app.overrideSource,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
if (!areDownloadsRunning &&
|
if (!areDownloadsRunning &&
|
||||||
prevApp == null &&
|
prevApp == null &&
|
||||||
@ -100,7 +104,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
bool isVersionDetectionStandard =
|
bool isVersionDetectionStandard =
|
||||||
app?.app.additionalSettings['versionDetection'] == true;
|
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) {
|
if (app != null && !_wasWebViewOpened) {
|
||||||
_wasWebViewOpened = true;
|
_wasWebViewOpened = true;
|
||||||
@ -122,11 +128,14 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (!upToDate) {
|
if (!upToDate) {
|
||||||
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
|
versionLines += '\n${app?.app.latestVersion} ${tr('latest')}';
|
||||||
}
|
}
|
||||||
String infoLines = tr('lastUpdateCheckX', args: [
|
String infoLines = tr(
|
||||||
|
'lastUpdateCheckX',
|
||||||
|
args: [
|
||||||
app?.app.lastUpdateCheck == null
|
app?.app.lastUpdateCheck == null
|
||||||
? tr('never')
|
? tr('never')
|
||||||
: '${app?.app.lastUpdateCheck?.toLocal()}'
|
: '${app?.app.lastUpdateCheck?.toLocal()}',
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
if (trackOnly) {
|
if (trackOnly) {
|
||||||
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
|
infoLines = '${tr('xIsTrackOnly', args: [tr('app')])}\n$infoLines';
|
||||||
}
|
}
|
||||||
@ -146,15 +155,14 @@ class _AppPageState extends State<AppPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
Text(
|
||||||
),
|
versionLines,
|
||||||
Text(versionLines,
|
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: Theme.of(context)
|
style: Theme.of(
|
||||||
.textTheme
|
context,
|
||||||
.bodyLarge!
|
).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold),
|
||||||
.copyWith(fontWeight: FontWeight.bold)),
|
),
|
||||||
changeLogFn != null || app?.app.releaseDate != null
|
changeLogFn != null || app?.app.releaseDate != null
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
onTap: changeLogFn,
|
onTap: changeLogFn,
|
||||||
@ -163,8 +171,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? tr('changes')
|
? tr('changes')
|
||||||
: app!.app.releaseDate!.toLocal().toString(),
|
: app!.app.releaseDate!.toLocal().toString(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style: Theme.of(context).textTheme.labelSmall!
|
||||||
Theme.of(context).textTheme.labelSmall!.copyWith(
|
.copyWith(
|
||||||
decoration: changeLogFn != null
|
decoration: changeLogFn != null
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: null,
|
: null,
|
||||||
@ -175,9 +183,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -193,8 +199,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
try {
|
try {
|
||||||
await appsProvider
|
await appsProvider.downloadAppAssets([
|
||||||
.downloadAppAssets([app!.app.id], context);
|
app!.app.id,
|
||||||
|
], context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}
|
}
|
||||||
@ -206,35 +213,34 @@ class _AppPageState extends State<AppPage> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: settingsProvider.highlightTouchTargets
|
color: settingsProvider.highlightTouchTargets
|
||||||
? (Theme.of(context).brightness ==
|
? (Theme.of(context).brightness == Brightness.light
|
||||||
Brightness.light
|
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: Theme.of(context).primaryColorLight)
|
: Theme.of(context).primaryColorLight)
|
||||||
.withAlpha(Theme.of(context).brightness ==
|
.withAlpha(
|
||||||
|
Theme.of(context).brightness ==
|
||||||
Brightness.light
|
Brightness.light
|
||||||
? 20
|
? 20
|
||||||
: 40)
|
: 40,
|
||||||
: null),
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
padding: settingsProvider.highlightTouchTargets
|
padding: settingsProvider.highlightTouchTargets
|
||||||
? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6)
|
? const EdgeInsetsDirectional.fromSTEB(12, 6, 12, 6)
|
||||||
: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6),
|
: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 6),
|
||||||
margin:
|
margin: const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0),
|
||||||
const EdgeInsetsDirectional.fromSTEB(0, 6, 0, 0),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('downloadX',
|
tr('downloadX', args: [tr('releaseAsset').toLowerCase()]),
|
||||||
args: [tr('releaseAsset').toLowerCase()]),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style:
|
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||||
Theme.of(context).textTheme.labelSmall!.copyWith(
|
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
))
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
const SizedBox(
|
|
||||||
height: 48,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
CategoryEditorSelector(
|
CategoryEditorSelector(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
preselected: app?.app.categories != null
|
preselected: app?.app.categories != null
|
||||||
@ -245,45 +251,52 @@ class _AppPageState extends State<AppPage> {
|
|||||||
app.app.categories = categories;
|
app.app.categories = categories;
|
||||||
appsProvider.saveApps([app.app]);
|
appsProvider.saveApps([app.app]);
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
if (app?.app.additionalSettings['about'] is String &&
|
if (app?.app.additionalSettings['about'] is String &&
|
||||||
app?.app.additionalSettings['about'].isNotEmpty)
|
app?.app.additionalSettings['about'].isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 48),
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
Clipboard.setData(ClipboardData(
|
Clipboard.setData(
|
||||||
text: app?.app.additionalSettings['about'] ?? ''));
|
ClipboardData(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
text: app?.app.additionalSettings['about'] ?? '',
|
||||||
content: Text(tr('copiedToClipboard')),
|
),
|
||||||
));
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(tr('copiedToClipboard'))),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Markdown(
|
child: Markdown(
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
blockquoteDecoration:
|
blockquoteDecoration: BoxDecoration(
|
||||||
BoxDecoration(color: Theme.of(context).cardColor),
|
color: Theme.of(context).cardColor,
|
||||||
textAlign: WrapAlignment.center),
|
),
|
||||||
|
textAlign: WrapAlignment.center,
|
||||||
|
),
|
||||||
data: app?.app.additionalSettings['about'],
|
data: app?.app.additionalSettings['about'],
|
||||||
onTapLink: (text, href, title) {
|
onTapLink: (text, href, title) {
|
||||||
if (href != null) {
|
if (href != null) {
|
||||||
launchUrlString(href,
|
launchUrlString(
|
||||||
mode: LaunchMode.externalApplication);
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extensionSet: md.ExtensionSet(
|
extensionSet: md.ExtensionSet(
|
||||||
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||||
[
|
[
|
||||||
md.EmojiSyntax(),
|
md.EmojiSyntax(),
|
||||||
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
))
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -296,8 +309,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(height: small ? 5 : 20),
|
SizedBox(height: small ? 5 : 20),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future:
|
future: appsProvider.updateAppIcon(app?.app.id, ignoreCache: true),
|
||||||
appsProvider.updateAppIcon(app?.app.id, ignoreCache: true),
|
|
||||||
builder: (ctx, val) {
|
builder: (ctx, val) {
|
||||||
return app?.icon != null
|
return app?.icon != null
|
||||||
? Row(
|
? Row(
|
||||||
@ -312,13 +324,13 @@ class _AppPageState extends State<AppPage> {
|
|||||||
height: small ? 70 : 150,
|
height: small ? 70 : 150,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
),
|
),
|
||||||
)
|
|
||||||
])
|
|
||||||
: Container();
|
|
||||||
}),
|
|
||||||
SizedBox(
|
|
||||||
height: small ? 10 : 25,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Container();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: small ? 10 : 25),
|
||||||
Text(
|
Text(
|
||||||
app?.name ?? tr('app'),
|
app?.name ?? tr('app'),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -326,41 +338,45 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? Theme.of(context).textTheme.displaySmall
|
? Theme.of(context).textTheme.displaySmall
|
||||||
: Theme.of(context).textTheme.displayLarge,
|
: Theme.of(context).textTheme.displayLarge,
|
||||||
),
|
),
|
||||||
Text(tr('byX', args: [app?.author ?? tr('unknown')]),
|
Text(
|
||||||
|
tr('byX', args: [app?.author ?? tr('unknown')]),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: small
|
style: small
|
||||||
? Theme.of(context).textTheme.headlineSmall
|
? Theme.of(context).textTheme.headlineSmall
|
||||||
: Theme.of(context).textTheme.headlineMedium),
|
: Theme.of(context).textTheme.headlineMedium,
|
||||||
const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (app?.app.url != null) {
|
if (app?.app.url != null) {
|
||||||
launchUrlString(app?.app.url ?? '',
|
launchUrlString(
|
||||||
mode: LaunchMode.externalApplication);
|
app?.app.url ?? '',
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
|
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(
|
||||||
content: Text(tr('copiedToClipboard')),
|
context,
|
||||||
));
|
).showSnackBar(SnackBar(content: Text(tr('copiedToClipboard'))));
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
app?.app.url ?? '',
|
app?.app.url ?? '',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontStyle: FontStyle.italic),
|
fontStyle: FontStyle.italic,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
app?.app.id ?? '',
|
app?.app.id ?? '',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
getInfoColumn(),
|
getInfoColumn(),
|
||||||
const SizedBox(height: 150)
|
const SizedBox(height: 150),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -368,7 +384,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? WebViewWidget(
|
? WebViewWidget(
|
||||||
key: ObjectKey(_webViewController),
|
key: ObjectKey(_webViewController),
|
||||||
controller: _webViewController
|
controller: _webViewController
|
||||||
..setBackgroundColor(Theme.of(context).colorScheme.surface))
|
..setBackgroundColor(Theme.of(context).colorScheme.surface),
|
||||||
|
)
|
||||||
: Container();
|
: Container();
|
||||||
|
|
||||||
showMarkUpdatedDialog() {
|
showMarkUpdatedDialog() {
|
||||||
@ -382,7 +399,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(tr('no'))),
|
child: Text(tr('no')),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
@ -393,18 +411,21 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(tr('yesMarkUpdated')))
|
child: Text(tr('yesMarkUpdated')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAdditionalOptionsDialog() async {
|
showAdditionalOptionsDialog() async {
|
||||||
return await showDialog<Map<String, dynamic>?>(
|
return await showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
var items =
|
var items = (source?.combinedAppSpecificSettingFormItems ?? []).map((
|
||||||
(source?.combinedAppSpecificSettingFormItems ?? []).map((row) {
|
row,
|
||||||
|
) {
|
||||||
row = row.map((e) {
|
row = row.map((e) {
|
||||||
if (app?.app.additionalSettings[e.key] != null) {
|
if (app?.app.additionalSettings[e.key] != null) {
|
||||||
e.defaultValue = app?.app.additionalSettings[e.key];
|
e.defaultValue = app?.app.additionalSettings[e.key];
|
||||||
@ -415,8 +436,11 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('additionalOptions'), items: items);
|
title: tr('additionalOptions'),
|
||||||
});
|
items: items,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
|
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
|
||||||
@ -440,8 +464,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (releaseDateVersionEnabled) {
|
if (releaseDateVersionEnabled) {
|
||||||
if (app.app.releaseDate != null) {
|
if (app.app.releaseDate != null) {
|
||||||
bool isUpdated = app.app.installedVersion == app.app.latestVersion;
|
bool isUpdated = app.app.installedVersion == app.app.latestVersion;
|
||||||
app.app.latestVersion =
|
app.app.latestVersion = app.app.releaseDate!.microsecondsSinceEpoch
|
||||||
app.app.releaseDate!.microsecondsSinceEpoch.toString();
|
.toString();
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
app.app.installedVersion = app.app.latestVersion;
|
app.app.installedVersion = app.app.latestVersion;
|
||||||
}
|
}
|
||||||
@ -461,7 +485,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getInstallOrUpdateButton() => TextButton(
|
getInstallOrUpdateButton() => TextButton(
|
||||||
onPressed: !updating &&
|
onPressed:
|
||||||
|
!updating &&
|
||||||
(app?.app.installedVersion == null ||
|
(app?.app.installedVersion == null ||
|
||||||
app?.app.installedVersion != app?.app.latestVersion) &&
|
app?.app.installedVersion != app?.app.latestVersion) &&
|
||||||
!areDownloadsRunning
|
!areDownloadsRunning
|
||||||
@ -488,17 +513,24 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(app?.app.installedVersion == null
|
child: Text(
|
||||||
|
app?.app.installedVersion == null
|
||||||
? !trackOnly
|
? !trackOnly
|
||||||
? tr('install')
|
? tr('install')
|
||||||
: tr('markInstalled')
|
: tr('markInstalled')
|
||||||
: !trackOnly
|
: !trackOnly
|
||||||
? tr('update')
|
? tr('update')
|
||||||
: tr('markUpdated')));
|
: tr('markUpdated'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
getBottomSheetMenu() => Padding(
|
getBottomSheetMenu() => Padding(
|
||||||
padding:
|
padding: EdgeInsets.fromLTRB(
|
||||||
EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
MediaQuery.of(context).padding.bottom,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -513,12 +545,12 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: app?.downloadProgress != null || updating
|
onPressed: app?.downloadProgress != null || updating
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
var values =
|
var values = await showAdditionalOptionsDialog();
|
||||||
await showAdditionalOptionsDialog();
|
|
||||||
handleAdditionalOptionChanges(values);
|
handleAdditionalOptionChanges(values);
|
||||||
},
|
},
|
||||||
tooltip: tr('additionalOptions'),
|
tooltip: tr('additionalOptions'),
|
||||||
icon: const Icon(Icons.edit)),
|
icon: const Icon(Icons.edit),
|
||||||
|
),
|
||||||
if (app != null && app.installedInfo != null)
|
if (app != null && app.installedInfo != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -542,13 +574,16 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(tr('continue')))
|
child: Text(tr('continue')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.more_horiz),
|
icon: const Icon(Icons.more_horiz),
|
||||||
tooltip: tr('more')),
|
tooltip: tr('more'),
|
||||||
|
),
|
||||||
if (app?.app.installedVersion != null &&
|
if (app?.app.installedVersion != null &&
|
||||||
app?.app.installedVersion != app?.app.latestVersion &&
|
app?.app.installedVersion != app?.app.latestVersion &&
|
||||||
!isVersionDetectionStandard &&
|
!isVersionDetectionStandard &&
|
||||||
@ -558,7 +593,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? null
|
? null
|
||||||
: showMarkUpdatedDialog,
|
: showMarkUpdatedDialog,
|
||||||
tooltip: tr('markUpdated'),
|
tooltip: tr('markUpdated'),
|
||||||
icon: const Icon(Icons.done)),
|
icon: const Icon(Icons.done),
|
||||||
|
),
|
||||||
if ((!isVersionDetectionStandard || trackOnly) &&
|
if ((!isVersionDetectionStandard || trackOnly) &&
|
||||||
app?.app.installedVersion != null &&
|
app?.app.installedVersion != null &&
|
||||||
app?.app.installedVersion == app?.app.latestVersion)
|
app?.app.installedVersion == app?.app.latestVersion)
|
||||||
@ -570,7 +606,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
appsProvider.saveApps([app.app]);
|
appsProvider.saveApps([app.app]);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.restore_rounded),
|
icon: const Icon(Icons.restore_rounded),
|
||||||
tooltip: tr('resetInstallStatus')),
|
tooltip: tr('resetInstallStatus'),
|
||||||
|
),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(child: getInstallOrUpdateButton()),
|
Expanded(child: getInstallOrUpdateButton()),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
@ -580,7 +617,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
: () {
|
: () {
|
||||||
appsProvider
|
appsProvider
|
||||||
.removeAppsWithModal(
|
.removeAppsWithModal(
|
||||||
context, app != null ? [app.app] : [])
|
context,
|
||||||
|
app != null ? [app.app] : [],
|
||||||
|
)
|
||||||
.then((value) {
|
.then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
@ -590,16 +629,21 @@ class _AppPageState extends State<AppPage> {
|
|||||||
tooltip: tr('remove'),
|
tooltip: tr('remove'),
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
),
|
),
|
||||||
])),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
if (app?.downloadProgress != null)
|
if (app?.downloadProgress != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: app!.downloadProgress! >= 0
|
value: app!.downloadProgress! >= 0
|
||||||
? app.downloadProgress! / 100
|
? app.downloadProgress! / 100
|
||||||
: null))
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
appScreenAppBar() => AppBar(
|
appScreenAppBar() => AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -619,14 +663,17 @@ class _AppPageState extends State<AppPage> {
|
|||||||
: CustomScrollView(
|
: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(children: [getFullInfoColumn()])),
|
child: Column(children: [getFullInfoColumn()]),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
getUpdate(app.app.id);
|
getUpdate(app.app.id);
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
bottomSheet: getBottomSheetMenu());
|
),
|
||||||
|
bottomSheet: getBottomSheetMenu(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -43,13 +43,22 @@ class _HomePageState extends State<HomePage> {
|
|||||||
bool isLinkActivity = false;
|
bool isLinkActivity = false;
|
||||||
|
|
||||||
List<NavigationPageItem> pages = [
|
List<NavigationPageItem> pages = [
|
||||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
|
||||||
AppsPage(key: GlobalKey<AppsPageState>())),
|
|
||||||
NavigationPageItem(
|
NavigationPageItem(
|
||||||
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
|
tr('appsString'),
|
||||||
|
Icons.apps,
|
||||||
|
AppsPage(key: GlobalKey<AppsPageState>()),
|
||||||
|
),
|
||||||
NavigationPageItem(
|
NavigationPageItem(
|
||||||
tr('importExport'), Icons.import_export, const ImportExportPage()),
|
tr('addApp'),
|
||||||
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
|
Icons.add,
|
||||||
|
AddAppPage(key: GlobalKey<AddAppPageState>()),
|
||||||
|
),
|
||||||
|
NavigationPageItem(
|
||||||
|
tr('importExport'),
|
||||||
|
Icons.import_export,
|
||||||
|
const ImportExportPage(),
|
||||||
|
),
|
||||||
|
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -73,14 +82,17 @@ class _HomePageState extends State<HomePage> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
'https://github.com/ImranR98/Obtainium/blob/main/README.md',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -100,11 +112,12 @@ class _HomePageState extends State<HomePage> {
|
|||||||
tr('settings'),
|
tr('settings'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -113,10 +126,12 @@ class _HomePageState extends State<HomePage> {
|
|||||||
sp.welcomeShown = true;
|
sp.welcomeShown = true;
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(tr('ok'))),
|
child: Text(tr('ok')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -126,13 +141,12 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
goToAddApp(String data) async {
|
goToAddApp(String data) async {
|
||||||
switchToPage(1);
|
switchToPage(1);
|
||||||
while (
|
while ((pages[1].widget.key as GlobalKey<AddAppPageState>?)
|
||||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
|
?.currentState ==
|
||||||
null) {
|
null) {
|
||||||
await Future.delayed(const Duration(microseconds: 1));
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
}
|
}
|
||||||
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
|
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState
|
||||||
?.currentState
|
|
||||||
?.linkFn(data);
|
?.linkFn(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,9 +163,13 @@ class _HomePageState extends State<HomePage> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('importX', args: [
|
title: tr(
|
||||||
action == 'app' ? tr('app') : tr('appsString')
|
'importX',
|
||||||
]),
|
args: [
|
||||||
|
(action == 'app' ? tr('app') : tr('appsString'))
|
||||||
|
.toLowerCase(),
|
||||||
|
],
|
||||||
|
),
|
||||||
items: const [],
|
items: const [],
|
||||||
additionalWidgets: [
|
additionalWidgets: [
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
@ -160,22 +178,29 @@ class _HomePageState extends State<HomePage> {
|
|||||||
Text(
|
Text(
|
||||||
dataStr,
|
dataStr,
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}) !=
|
},
|
||||||
|
) !=
|
||||||
null) {
|
null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.read<AppsProvider>();
|
||||||
var result = await appsProvider.import(action == 'app'
|
var result = await appsProvider.import(
|
||||||
|
action == 'app'
|
||||||
? '{ "apps": [$dataStr] }'
|
? '{ "apps": [$dataStr] }'
|
||||||
: '{ "apps": $dataStr }');
|
: '{ "apps": $dataStr }',
|
||||||
|
);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showMessage(
|
showMessage(
|
||||||
tr('importedX', args: [plural('apps', result.key.length)]),
|
tr(
|
||||||
context);
|
'importedX',
|
||||||
|
args: [plural('apps', result.key.length).toLowerCase()],
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
await appsProvider
|
await appsProvider
|
||||||
.checkUpdates(specificIds: result.key.map((e) => e.id).toList())
|
.checkUpdates(specificIds: result.key.map((e) => e.id).toList())
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
@ -210,15 +235,16 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsReversing(int targetIndex) {
|
void setIsReversing(int targetIndex) {
|
||||||
bool reversing = selectedIndexHistory.isNotEmpty &&
|
bool reversing =
|
||||||
|
selectedIndexHistory.isNotEmpty &&
|
||||||
selectedIndexHistory.last > targetIndex;
|
selectedIndexHistory.last > targetIndex;
|
||||||
setState(() {
|
setState(() {
|
||||||
isReversing = reversing;
|
isReversing = reversing;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToPage(int index) async {
|
Future<void> switchToPage(int index) async {
|
||||||
setIsReversing(index);
|
setIsReversing(index);
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
||||||
@ -263,12 +289,13 @@ class _HomePageState extends State<HomePage> {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: PageTransitionSwitcher(
|
body: PageTransitionSwitcher(
|
||||||
duration: Duration(
|
duration: Duration(
|
||||||
milliseconds:
|
milliseconds: settingsProvider.disablePageTransitions ? 0 : 300,
|
||||||
settingsProvider.disablePageTransitions ? 0 : 300),
|
),
|
||||||
reverse: settingsProvider.reversePageTransitions
|
reverse: settingsProvider.reversePageTransitions
|
||||||
? !isReversing
|
? !isReversing
|
||||||
: isReversing,
|
: isReversing,
|
||||||
transitionBuilder: (
|
transitionBuilder:
|
||||||
|
(
|
||||||
Widget child,
|
Widget child,
|
||||||
Animation<double> animation,
|
Animation<double> animation,
|
||||||
Animation<double> secondaryAnimation,
|
Animation<double> secondaryAnimation,
|
||||||
@ -281,22 +308,25 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: pages
|
child: pages
|
||||||
.elementAt(selectedIndexHistory.isEmpty
|
.elementAt(
|
||||||
? 0
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
: selectedIndexHistory.last)
|
)
|
||||||
.widget,
|
.widget,
|
||||||
),
|
),
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
destinations: pages
|
destinations: pages
|
||||||
.map((e) =>
|
.map(
|
||||||
NavigationDestination(icon: Icon(e.icon), label: e.title))
|
(e) =>
|
||||||
|
NavigationDestination(icon: Icon(e.icon), label: e.title),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (int index) async {
|
onDestinationSelected: (int index) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
switchToPage(index);
|
switchToPage(index);
|
||||||
},
|
},
|
||||||
selectedIndex:
|
selectedIndex: selectedIndexHistory.isEmpty
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
? 0
|
||||||
|
: selectedIndexHistory.last,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
@ -305,19 +335,21 @@ class _HomePageState extends State<HomePage> {
|
|||||||
selectedIndexHistory.last == 1) {
|
selectedIndexHistory.last == 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
setIsReversing(selectedIndexHistory.length >= 2
|
setIsReversing(
|
||||||
|
selectedIndexHistory.length >= 2
|
||||||
? selectedIndexHistory.reversed.toList()[1]
|
? selectedIndexHistory.reversed.toList()[1]
|
||||||
: 0);
|
: 0,
|
||||||
|
);
|
||||||
if (selectedIndexHistory.isNotEmpty) {
|
if (selectedIndexHistory.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIndexHistory.removeLast();
|
selectedIndexHistory.removeLast();
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !(pages[0].widget.key as GlobalKey<AppsPageState>)
|
return !(pages[0].widget.key as GlobalKey<AppsPageState>).currentState!
|
||||||
.currentState
|
.clearSelected();
|
||||||
?.clearSelected();
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -52,7 +52,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
title: tr('importFromURLList'),
|
title: tr('importFromURLList'),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('appURLList',
|
GeneratedFormTextField(
|
||||||
|
'appURLList',
|
||||||
defaultValue: initValue ?? '',
|
defaultValue: initValue ?? '',
|
||||||
label: tr('appURLList'),
|
label: tr('appURLList'),
|
||||||
max: 7,
|
max: 7,
|
||||||
@ -69,32 +70,46 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).then((values) {
|
},
|
||||||
|
).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
var urls = (values['appURLList'] as String).split('\n');
|
var urls = (values['appURLList'] as String).split('\n');
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
appsProvider.addAppsByURL(urls).then((errors) {
|
appsProvider
|
||||||
|
.addAppsByURL(urls)
|
||||||
|
.then((errors) {
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
showMessage(tr('importedX', args: [plural('apps', urls.length)]),
|
showMessage(
|
||||||
context);
|
tr(
|
||||||
|
'importedX',
|
||||||
|
args: [plural('apps', urls.length).toLowerCase()],
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return ImportErrorDialog(
|
return ImportErrorDialog(
|
||||||
urlsLength: urls.length, errors: errors);
|
urlsLength: urls.length,
|
||||||
});
|
errors: errors,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
})
|
||||||
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
})
|
||||||
|
.whenComplete(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = false;
|
importInProgress = false;
|
||||||
});
|
});
|
||||||
@ -109,19 +124,23 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
.export(
|
.export(
|
||||||
pickOnly:
|
pickOnly:
|
||||||
pickOnly || (await settingsProvider.getExportDir()) == null,
|
pickOnly || (await settingsProvider.getExportDir()) == null,
|
||||||
sp: settingsProvider)
|
sp: settingsProvider,
|
||||||
|
)
|
||||||
.then((String? result) {
|
.then((String? result) {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
showMessage(tr('exportedTo', args: [result]), context);
|
showMessage(tr('exportedTo', args: [result]), context);
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
})
|
||||||
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runObtainiumImport() {
|
runObtainiumImport() {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
FilePicker.platform.pickFiles().then((result) {
|
FilePicker.platform
|
||||||
|
.pickFiles()
|
||||||
|
.then((result) {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
@ -143,17 +162,18 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
});
|
});
|
||||||
appsProvider.addMissingCategories(settingsProvider);
|
appsProvider.addMissingCategories(settingsProvider);
|
||||||
showMessage(
|
showMessage(
|
||||||
'${tr('importedX', args: [
|
'${tr('importedX', args: [plural('apps', value.key.length).toLowerCase()])}${value.value ? ' + ${tr('settings').toLowerCase()}' : ''}',
|
||||||
plural('apps', value.key.length)
|
context,
|
||||||
])}${value.value ? ' + ${tr('settings')}' : ''}',
|
);
|
||||||
context);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// User canceled the picker
|
// User canceled the picker
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
})
|
||||||
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
})
|
||||||
|
.whenComplete(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = false;
|
importInProgress = false;
|
||||||
});
|
});
|
||||||
@ -166,8 +186,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
urlListImport(
|
urlListImport(
|
||||||
overrideInitValid: true,
|
overrideInitValid: true,
|
||||||
initValue: RegExp('https?://[^"]+')
|
initValue: RegExp('https?://[^"]+')
|
||||||
.allMatches(
|
.allMatches(File(result.files.single.path!).readAsStringSync())
|
||||||
File(result.files.single.path!).readAsStringSync())
|
|
||||||
.map((e) => e.input.substring(e.start, e.end))
|
.map((e) => e.input.substring(e.start, e.end))
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList()
|
.toList()
|
||||||
@ -178,7 +197,9 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}).join('\n'));
|
})
|
||||||
|
.join('\n'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -192,33 +213,43 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
title: tr('searchX', args: [source.name]),
|
title: tr('searchX', args: [source.name]),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('searchQuery',
|
GeneratedFormTextField(
|
||||||
|
'searchQuery',
|
||||||
label: tr('searchQuery'),
|
label: tr('searchQuery'),
|
||||||
required: source.name != FDroidRepo().name)
|
required: source.name != FDroidRepo().name,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
...source.searchQuerySettingFormItems.map((e) => [e]),
|
...source.searchQuerySettingFormItems.map((e) => [e]),
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('url',
|
GeneratedFormTextField(
|
||||||
|
'url',
|
||||||
label: source.hosts.isNotEmpty
|
label: source.hosts.isNotEmpty
|
||||||
? tr('overrideSource')
|
? tr('overrideSource')
|
||||||
: plural('url', 1).substring(2),
|
: plural('url', 1).substring(2),
|
||||||
defaultValue:
|
defaultValue: source.hosts.isNotEmpty
|
||||||
source.hosts.isNotEmpty ? source.hosts[0] : '',
|
? source.hosts[0]
|
||||||
required: true)
|
: '',
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
if (source.hosts.isEmpty || values['url'] != source.hosts[0]) {
|
if (source.hosts.isEmpty || values['url'] != source.hosts[0]) {
|
||||||
source = sourceProvider.getSource(values['url'],
|
source = sourceProvider.getSource(
|
||||||
overrideSource: source.runtimeType.toString());
|
values['url'],
|
||||||
|
overrideSource: source.runtimeType.toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
var urlsWithDescriptions = await source
|
var urlsWithDescriptions = await source.search(
|
||||||
.search(values['searchQuery'] as String, querySettings: values);
|
values['searchQuery'] as String,
|
||||||
|
querySettings: values,
|
||||||
|
);
|
||||||
if (urlsWithDescriptions.isNotEmpty) {
|
if (urlsWithDescriptions.isNotEmpty) {
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@ -229,24 +260,33 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
entries: urlsWithDescriptions,
|
entries: urlsWithDescriptions,
|
||||||
selectedByDefault: false,
|
selectedByDefault: false,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||||
var errors = await appsProvider.addAppsByURL(selectedUrls,
|
var errors = await appsProvider.addAppsByURL(
|
||||||
sourceOverride: source);
|
selectedUrls,
|
||||||
|
sourceOverride: source,
|
||||||
|
);
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showMessage(
|
showMessage(
|
||||||
tr('importedX',
|
tr(
|
||||||
args: [plural('apps', selectedUrls.length)]),
|
'importedX',
|
||||||
context);
|
args: [plural('apps', selectedUrls.length)],
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return ImportErrorDialog(
|
return ImportErrorDialog(
|
||||||
urlsLength: selectedUrls.length, errors: errors);
|
urlsLength: selectedUrls.length,
|
||||||
});
|
errors: errors,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -256,7 +296,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
}()
|
}()
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
})
|
||||||
|
.whenComplete(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = false;
|
importInProgress = false;
|
||||||
});
|
});
|
||||||
@ -274,42 +315,53 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
.map((e) => [GeneratedFormTextField(e, label: e)])
|
.map((e) => [GeneratedFormTextField(e, label: e)])
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
var urlsWithDescriptions = await source.getUrlsWithDescriptions(
|
var urlsWithDescriptions = await source.getUrlsWithDescriptions(
|
||||||
values.values.map((e) => e.toString()).toList());
|
values.values.map((e) => e.toString()).toList(),
|
||||||
|
);
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog<List<String>?>(
|
await showDialog<List<String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return SelectionModal(entries: urlsWithDescriptions);
|
return SelectionModal(entries: urlsWithDescriptions);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (selectedUrls != null) {
|
if (selectedUrls != null) {
|
||||||
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
||||||
if (errors.isEmpty) {
|
if (errors.isEmpty) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showMessage(
|
showMessage(
|
||||||
tr('importedX', args: [plural('apps', selectedUrls.length)]),
|
tr(
|
||||||
context);
|
'importedX',
|
||||||
|
args: [plural('apps', selectedUrls.length)],
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return ImportErrorDialog(
|
return ImportErrorDialog(
|
||||||
urlsLength: selectedUrls.length, errors: errors);
|
urlsLength: selectedUrls.length,
|
||||||
});
|
errors: errors,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
})
|
||||||
|
.whenComplete(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = false;
|
importInProgress = false;
|
||||||
});
|
});
|
||||||
@ -323,12 +375,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('importExport')),
|
CustomAppBar(title: tr('importExport')),
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -342,34 +394,38 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: outlineButtonStyle,
|
style: outlineButtonStyle,
|
||||||
onPressed: appsProvider.apps.isEmpty ||
|
onPressed:
|
||||||
|
appsProvider.apps.isEmpty ||
|
||||||
importInProgress
|
importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
runObtainiumExport(pickOnly: true);
|
runObtainiumExport(pickOnly: true);
|
||||||
},
|
},
|
||||||
child: Text(tr('pickExportDir'),
|
child: Text(
|
||||||
textAlign: TextAlign.center),
|
tr('pickExportDir'),
|
||||||
)),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: outlineButtonStyle,
|
style: outlineButtonStyle,
|
||||||
onPressed: appsProvider.apps.isEmpty ||
|
onPressed:
|
||||||
|
appsProvider.apps.isEmpty ||
|
||||||
importInProgress ||
|
importInProgress ||
|
||||||
snapshot.data == null
|
snapshot.data == null
|
||||||
? null
|
? null
|
||||||
: runObtainiumExport,
|
: runObtainiumExport,
|
||||||
child: Text(tr('obtainiumExport'),
|
child: Text(
|
||||||
textAlign: TextAlign.center),
|
tr('obtainiumExport'),
|
||||||
)),
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -378,8 +434,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: runObtainiumImport,
|
: runObtainiumImport,
|
||||||
child: Text(tr('obtainiumImport'),
|
child: Text(
|
||||||
textAlign: TextAlign.center))),
|
tr('obtainiumImport'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (snapshot.data != null)
|
if (snapshot.data != null)
|
||||||
@ -394,35 +454,32 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
label: tr('autoExportOnChanges'),
|
label: tr('autoExportOnChanges'),
|
||||||
defaultValue: settingsProvider
|
defaultValue: settingsProvider
|
||||||
.autoExportOnChanges,
|
.autoExportOnChanges,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch(
|
GeneratedFormSwitch(
|
||||||
'exportSettings',
|
'exportSettings',
|
||||||
label: tr('includeSettings'),
|
label: tr('includeSettings'),
|
||||||
defaultValue: settingsProvider
|
defaultValue:
|
||||||
.exportSettings,
|
settingsProvider.exportSettings,
|
||||||
)
|
),
|
||||||
]
|
|
||||||
],
|
],
|
||||||
onValueChanges:
|
],
|
||||||
(value, valid, isBuilding) {
|
onValueChanges: (value, valid, isBuilding) {
|
||||||
if (valid && !isBuilding) {
|
if (valid && !isBuilding) {
|
||||||
if (value['autoExportOnChanges'] !=
|
if (value['autoExportOnChanges'] !=
|
||||||
null) {
|
null) {
|
||||||
settingsProvider
|
settingsProvider.autoExportOnChanges =
|
||||||
.autoExportOnChanges = value[
|
value['autoExportOnChanges'] ==
|
||||||
'autoExportOnChanges'] ==
|
|
||||||
true;
|
true;
|
||||||
}
|
}
|
||||||
if (value['exportSettings'] !=
|
if (value['exportSettings'] != null) {
|
||||||
null) {
|
|
||||||
settingsProvider.exportSettings =
|
settingsProvider.exportSettings =
|
||||||
value['exportSettings'] ==
|
value['exportSettings'] == true;
|
||||||
true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -432,21 +489,15 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
if (importInProgress)
|
if (importInProgress)
|
||||||
const Column(
|
const Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(height: 14),
|
||||||
height: 14,
|
|
||||||
),
|
|
||||||
LinearProgressIndicator(),
|
LinearProgressIndicator(),
|
||||||
SizedBox(
|
SizedBox(height: 14),
|
||||||
height: 14,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const Divider(
|
const Divider(height: 32),
|
||||||
height: 32,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -455,63 +506,58 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
var searchSourceName =
|
var searchSourceName =
|
||||||
await showDialog<
|
await showDialog<List<String>?>(
|
||||||
List<String>?>(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder: (BuildContext ctx) {
|
||||||
(BuildContext
|
|
||||||
ctx) {
|
|
||||||
return SelectionModal(
|
return SelectionModal(
|
||||||
title: tr(
|
title: tr(
|
||||||
'selectX',
|
'selectX',
|
||||||
args: [
|
args: [tr('source')],
|
||||||
tr('source')
|
),
|
||||||
]),
|
entries: sourceStrings,
|
||||||
entries:
|
selectedByDefault: false,
|
||||||
sourceStrings,
|
onlyOneSelectionAllowed: true,
|
||||||
selectedByDefault:
|
titlesAreLinks: false,
|
||||||
false,
|
|
||||||
onlyOneSelectionAllowed:
|
|
||||||
true,
|
|
||||||
titlesAreLinks:
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}) ??
|
},
|
||||||
|
) ??
|
||||||
[];
|
[];
|
||||||
var searchSource =
|
var searchSource = sourceProvider
|
||||||
sourceProvider.sources
|
.sources
|
||||||
.where((e) =>
|
.where(
|
||||||
searchSourceName
|
(e) => searchSourceName.contains(
|
||||||
.contains(
|
e.name,
|
||||||
e.name))
|
),
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (searchSource.isNotEmpty) {
|
if (searchSource.isNotEmpty) {
|
||||||
runSourceSearch(
|
runSourceSearch(searchSource[0]);
|
||||||
searchSource[0]);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(tr('searchX', args: [
|
child: Text(
|
||||||
tr('source').toLowerCase()
|
tr(
|
||||||
])))),
|
'searchX',
|
||||||
|
args: [tr('source').toLowerCase()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: importInProgress ? null : urlListImport,
|
||||||
importInProgress ? null : urlListImport,
|
child: Text(tr('importFromURLList')),
|
||||||
child: Text(
|
),
|
||||||
tr('importFromURLList'),
|
|
||||||
)),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed: importInProgress ? null : runUrlImport,
|
||||||
importInProgress ? null : runUrlImport,
|
child: Text(tr('importFromURLsInFile')),
|
||||||
child: Text(
|
),
|
||||||
tr('importFromURLsInFile'),
|
|
||||||
)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...sourceProvider.massUrlSources.map((source) => Column(
|
...sourceProvider.massUrlSources.map(
|
||||||
|
(source) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -521,29 +567,38 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
: () {
|
: () {
|
||||||
runMassSourceImport(source);
|
runMassSourceImport(source);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(tr('importX', args: [source.name])),
|
||||||
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 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 {
|
class ImportErrorDialog extends StatefulWidget {
|
||||||
const ImportErrorDialog(
|
const ImportErrorDialog({
|
||||||
{super.key, required this.urlsLength, required this.errors});
|
super.key,
|
||||||
|
required this.urlsLength,
|
||||||
|
required this.errors,
|
||||||
|
});
|
||||||
|
|
||||||
final int urlsLength;
|
final int urlsLength;
|
||||||
final List<List<String>> errors;
|
final List<List<String>> errors;
|
||||||
@ -558,13 +613,17 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(tr('importErrors')),
|
title: Text(tr('importErrors')),
|
||||||
content:
|
content: Column(
|
||||||
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr('importedXOfYApps', args: [
|
tr(
|
||||||
|
'importedXOfYApps',
|
||||||
|
args: [
|
||||||
(widget.urlsLength - widget.errors.length).toString(),
|
(widget.urlsLength - widget.errors.length).toString(),
|
||||||
widget.urlsLength.toString()
|
widget.urlsLength.toString(),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -576,23 +635,21 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
Text(e[0]),
|
Text(e[0]),
|
||||||
Text(
|
Text(e[1], style: const TextStyle(fontStyle: FontStyle.italic)),
|
||||||
e[1],
|
],
|
||||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
);
|
||||||
)
|
}),
|
||||||
]);
|
],
|
||||||
})
|
),
|
||||||
]),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
},
|
},
|
||||||
child: Text(tr('ok')))
|
child: Text(tr('ok')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -600,14 +657,15 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SelectionModal extends StatefulWidget {
|
class SelectionModal extends StatefulWidget {
|
||||||
SelectionModal(
|
SelectionModal({
|
||||||
{super.key,
|
super.key,
|
||||||
required this.entries,
|
required this.entries,
|
||||||
this.selectedByDefault = true,
|
this.selectedByDefault = true,
|
||||||
this.onlyOneSelectionAllowed = false,
|
this.onlyOneSelectionAllowed = false,
|
||||||
this.titlesAreLinks = true,
|
this.titlesAreLinks = true,
|
||||||
this.title,
|
this.title,
|
||||||
this.deselectThese = const []});
|
this.deselectThese = const [],
|
||||||
|
});
|
||||||
|
|
||||||
String? title;
|
String? title;
|
||||||
Map<String, List<String>> entries;
|
Map<String, List<String>> entries;
|
||||||
@ -632,14 +690,15 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
() =>
|
() =>
|
||||||
widget.selectedByDefault &&
|
widget.selectedByDefault &&
|
||||||
!widget.onlyOneSelectionAllowed &&
|
!widget.onlyOneSelectionAllowed &&
|
||||||
!widget.deselectThese.contains(entry.key));
|
!widget.deselectThese.contains(entry.key),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
|
if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
|
||||||
selectOnlyOne(widget.entries.entries.first.key);
|
selectOnlyOne(widget.entries.entries.first.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOnlyOne(String url) {
|
void selectOnlyOne(String url) {
|
||||||
for (var e in entrySelections.keys) {
|
for (var e in entrySelections.keys) {
|
||||||
entrySelections[e] = e.key == url;
|
entrySelections[e] = e.key == url;
|
||||||
}
|
}
|
||||||
@ -658,8 +717,10 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
entrySelections.forEach((key, value) {
|
entrySelections.forEach((key, value) {
|
||||||
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
var searchableText = key.value.isEmpty ? key.key : key.value[0];
|
||||||
if (filterRegex.isEmpty ||
|
if (filterRegex.isEmpty ||
|
||||||
RegExp(filterRegex, caseSensitive: false)
|
RegExp(
|
||||||
.hasMatch(searchableText)) {
|
filterRegex,
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(searchableText)) {
|
||||||
filteredEntrySelections.putIfAbsent(key, () => value);
|
filteredEntrySelections.putIfAbsent(key, () => value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -667,19 +728,22 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(widget.title ?? tr('pick')),
|
title: Text(widget.title ?? tr('pick')),
|
||||||
content: Column(children: [
|
content: Column(
|
||||||
|
children: [
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('filter',
|
GeneratedFormTextField(
|
||||||
|
'filter',
|
||||||
label: tr('filter'),
|
label: tr('filter'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
]
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
onValueChanges: (value, valid, isBuilding) {
|
onValueChanges: (value, valid, isBuilding) {
|
||||||
if (valid && !isBuilding) {
|
if (valid && !isBuilding) {
|
||||||
@ -689,7 +753,8 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
...filteredEntrySelections.keys.map((entry) {
|
...filteredEntrySelections.keys.map((entry) {
|
||||||
selectThis(bool? value) {
|
selectThis(bool? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -706,8 +771,10 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
onTap: !widget.titlesAreLinks
|
onTap: !widget.titlesAreLinks
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
launchUrlString(entry.key,
|
launchUrlString(
|
||||||
mode: LaunchMode.externalApplication);
|
entry.key,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -718,17 +785,21 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
decoration: widget.titlesAreLinks
|
decoration: widget.titlesAreLinks
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: null,
|
: null,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
),
|
),
|
||||||
if (widget.titlesAreLinks)
|
if (widget.titlesAreLinks)
|
||||||
Text(
|
Text(
|
||||||
Uri.parse(entry.key).host,
|
Uri.parse(entry.key).host,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
decoration: TextDecoration.underline,
|
||||||
)
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
var descriptionText = entry.value.length <= 1
|
var descriptionText = entry.value.length <= 1
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
@ -737,11 +808,14 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
? '${entry.value[1].substring(0, 128)}...'
|
? '${entry.value[1].substring(0, 128)}...'
|
||||||
: entry.value[1],
|
: entry.value[1],
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var selectedEntries =
|
var selectedEntries = entrySelections.entries
|
||||||
entrySelections.entries.where((e) => e.value).toList();
|
.where((e) => e.value)
|
||||||
|
.toList();
|
||||||
|
|
||||||
var singleSelectTile = ListTile(
|
var singleSelectTile = ListTile(
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
@ -775,23 +849,21 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var multiSelectTile = Row(children: [
|
var multiSelectTile = Row(
|
||||||
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: entrySelections[entry],
|
value: entrySelections[entry],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
selectThis(value);
|
selectThis(value);
|
||||||
}),
|
},
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: widget.titlesAreLinks
|
onTap: widget.titlesAreLinks
|
||||||
? null
|
? null
|
||||||
@ -808,38 +880,48 @@ class _SelectionModalState extends State<SelectionModal> {
|
|||||||
},
|
},
|
||||||
child: descriptionText,
|
child: descriptionText,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 8,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
))
|
),
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return widget.onlyOneSelectionAllowed
|
return widget.onlyOneSelectionAllowed
|
||||||
? singleSelectTile
|
? singleSelectTile
|
||||||
: multiSelectTile;
|
: multiSelectTile;
|
||||||
})
|
}),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(tr('cancel'))),
|
child: Text(tr('cancel')),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: entrySelections.values.where((b) => b).isEmpty
|
onPressed: entrySelections.values.where((b) => b).isEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
Navigator.of(context).pop(entrySelections.entries
|
Navigator.of(context).pop(
|
||||||
|
entrySelections.entries
|
||||||
.where((entry) => entry.value)
|
.where((entry) => entry.value)
|
||||||
.map((e) => e.key.key)
|
.map((e) => e.key.key)
|
||||||
.toList());
|
.toList(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text(widget.onlyOneSelectionAllowed
|
child: Text(
|
||||||
|
widget.onlyOneSelectionAllowed
|
||||||
? tr('pick')
|
? tr('pick')
|
||||||
: tr('selectX', args: [
|
: tr(
|
||||||
entrySelections.values.where((b) => b).length.toString()
|
'selectX',
|
||||||
])))
|
args: [
|
||||||
|
entrySelections.values.where((b) => b).length.toString(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ class Log {
|
|||||||
idColumn: id,
|
idColumn: id,
|
||||||
levelColumn: level.index,
|
levelColumn: level.index,
|
||||||
messageColumn: message,
|
messageColumn: message,
|
||||||
timestampColumn: timestamp.millisecondsSinceEpoch
|
timestampColumn: timestamp.millisecondsSinceEpoch,
|
||||||
};
|
};
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@ -33,8 +33,9 @@ class Log {
|
|||||||
id = map[idColumn] as int;
|
id = map[idColumn] as int;
|
||||||
level = LogLevels.values.elementAt(map[levelColumn] as int);
|
level = LogLevels.values.elementAt(map[levelColumn] as int);
|
||||||
message = map[messageColumn] as String;
|
message = map[messageColumn] as String;
|
||||||
timestamp =
|
timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
DateTime.fromMillisecondsSinceEpoch(map[timestampColumn] as int);
|
map[timestampColumn] as int,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -51,7 +52,9 @@ class LogsProvider {
|
|||||||
Database? db;
|
Database? db;
|
||||||
|
|
||||||
Future<Database> getDB() async {
|
Future<Database> getDB() async {
|
||||||
db ??= await openDatabase(dbPath, version: 1,
|
db ??= await openDatabase(
|
||||||
|
dbPath,
|
||||||
|
version: 1,
|
||||||
onCreate: (Database db, int version) async {
|
onCreate: (Database db, int version) async {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
create table if not exists $logTable (
|
create table if not exists $logTable (
|
||||||
@ -60,7 +63,8 @@ create table if not exists $logTable (
|
|||||||
$messageColumn text not null,
|
$messageColumn text not null,
|
||||||
$timestampColumn integer not null)
|
$timestampColumn integer not null)
|
||||||
''');
|
''');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return db!;
|
return db!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,27 +79,38 @@ create table if not exists $logTable (
|
|||||||
|
|
||||||
Future<List<Log>> get({DateTime? before, DateTime? after}) async {
|
Future<List<Log>> get({DateTime? before, DateTime? after}) async {
|
||||||
var where = getWhereDates(before: before, after: after);
|
var where = getWhereDates(before: before, after: after);
|
||||||
return (await (await getDB())
|
return (await (await getDB()).query(
|
||||||
.query(logTable, where: where.key, whereArgs: where.value))
|
logTable,
|
||||||
.map((e) => Log.fromMap(e))
|
where: where.key,
|
||||||
.toList();
|
whereArgs: where.value,
|
||||||
|
)).map((e) => Log.fromMap(e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> clear({DateTime? before, DateTime? after}) async {
|
Future<int> clear({DateTime? before, DateTime? after}) async {
|
||||||
var where = getWhereDates(before: before, after: after);
|
var where = getWhereDates(before: before, after: after);
|
||||||
var res = await (await getDB())
|
var res = await (await getDB()).delete(
|
||||||
.delete(logTable, where: where.key, whereArgs: where.value);
|
logTable,
|
||||||
|
where: where.key,
|
||||||
|
whereArgs: where.value,
|
||||||
|
);
|
||||||
if (res > 0) {
|
if (res > 0) {
|
||||||
add(plural('clearedNLogsBeforeXAfterY', res,
|
add(
|
||||||
|
plural(
|
||||||
|
'clearedNLogsBeforeXAfterY',
|
||||||
|
res,
|
||||||
namedArgs: {'before': before.toString(), 'after': after.toString()},
|
namedArgs: {'before': before.toString(), 'after': after.toString()},
|
||||||
name: 'n'));
|
name: 'n',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MapEntry<String?, List<int>?> getWhereDates(
|
MapEntry<String?, List<int>?> getWhereDates({
|
||||||
{DateTime? before, DateTime? after}) {
|
DateTime? before,
|
||||||
|
DateTime? after,
|
||||||
|
}) {
|
||||||
List<String> where = [];
|
List<String> where = [];
|
||||||
List<int> whereArgs = [];
|
List<int> whereArgs = [];
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
|
@ -20,9 +20,18 @@ class ObtainiumNotification {
|
|||||||
bool onlyAlertOnce;
|
bool onlyAlertOnce;
|
||||||
String? payload;
|
String? payload;
|
||||||
|
|
||||||
ObtainiumNotification(this.id, this.title, this.message, this.channelCode,
|
ObtainiumNotification(
|
||||||
this.channelName, this.channelDescription, this.importance,
|
this.id,
|
||||||
{this.onlyAlertOnce = false, this.progPercent, this.payload});
|
this.title,
|
||||||
|
this.message,
|
||||||
|
this.channelCode,
|
||||||
|
this.channelName,
|
||||||
|
this.channelDescription,
|
||||||
|
this.importance, {
|
||||||
|
this.onlyAlertOnce = false,
|
||||||
|
this.progPercent,
|
||||||
|
this.payload,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateNotification extends ObtainiumNotification {
|
class UpdateNotification extends ObtainiumNotification {
|
||||||
@ -34,13 +43,17 @@ class UpdateNotification extends ObtainiumNotification {
|
|||||||
'UPDATES_AVAILABLE',
|
'UPDATES_AVAILABLE',
|
||||||
tr('updatesAvailableNotifChannel'),
|
tr('updatesAvailableNotifChannel'),
|
||||||
tr('updatesAvailableNotifDescription'),
|
tr('updatesAvailableNotifDescription'),
|
||||||
Importance.max) {
|
Importance.max,
|
||||||
|
) {
|
||||||
message = updates.isEmpty
|
message = updates.isEmpty
|
||||||
? tr('noNewUpdates')
|
? tr('noNewUpdates')
|
||||||
: updates.length == 1
|
: updates.length == 1
|
||||||
? tr('xHasAnUpdate', args: [updates[0].finalName])
|
? tr('xHasAnUpdate', args: [updates[0].finalName])
|
||||||
: plural('xAndNMoreUpdatesAvailable', updates.length - 1,
|
: plural(
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
'xAndNMoreUpdatesAvailable',
|
||||||
|
updates.length - 1,
|
||||||
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,14 +66,18 @@ class SilentUpdateNotification extends ObtainiumNotification {
|
|||||||
'APPS_UPDATED',
|
'APPS_UPDATED',
|
||||||
tr('appsUpdatedNotifChannel'),
|
tr('appsUpdatedNotifChannel'),
|
||||||
tr('appsUpdatedNotifDescription'),
|
tr('appsUpdatedNotifDescription'),
|
||||||
Importance.defaultImportance) {
|
Importance.defaultImportance,
|
||||||
|
) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr(succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY',
|
? tr(
|
||||||
args: [updates[0].finalName, updates[0].latestVersion])
|
succeeded ? 'xWasUpdatedToY' : 'xWasNotUpdatedToY',
|
||||||
|
args: [updates[0].finalName, updates[0].latestVersion],
|
||||||
|
)
|
||||||
: plural(
|
: plural(
|
||||||
succeeded ? 'xAndNMoreUpdatesInstalled' : "xAndNMoreUpdatesFailed",
|
succeeded ? 'xAndNMoreUpdatesInstalled' : "xAndNMoreUpdatesFailed",
|
||||||
updates.length - 1,
|
updates.length - 1,
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,12 +90,18 @@ class SilentUpdateAttemptNotification extends ObtainiumNotification {
|
|||||||
'APPS_POSSIBLY_UPDATED',
|
'APPS_POSSIBLY_UPDATED',
|
||||||
tr('appsPossiblyUpdatedNotifChannel'),
|
tr('appsPossiblyUpdatedNotifChannel'),
|
||||||
tr('appsPossiblyUpdatedNotifDescription'),
|
tr('appsPossiblyUpdatedNotifDescription'),
|
||||||
Importance.defaultImportance) {
|
Importance.defaultImportance,
|
||||||
|
) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr('xWasPossiblyUpdatedToY',
|
? tr(
|
||||||
args: [updates[0].finalName, updates[0].latestVersion])
|
'xWasPossiblyUpdatedToY',
|
||||||
: plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1,
|
args: [updates[0].finalName, updates[0].latestVersion],
|
||||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
)
|
||||||
|
: plural(
|
||||||
|
'xAndNMoreUpdatesPossiblyInstalled',
|
||||||
|
updates.length - 1,
|
||||||
|
args: [updates[0].finalName, (updates.length - 1).toString()],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +115,8 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
|||||||
tr('errorCheckingUpdatesNotifChannel'),
|
tr('errorCheckingUpdatesNotifChannel'),
|
||||||
tr('errorCheckingUpdatesNotifDescription'),
|
tr('errorCheckingUpdatesNotifDescription'),
|
||||||
Importance.high,
|
Importance.high,
|
||||||
payload: "${tr('errorCheckingUpdates')}\n$error");
|
payload: "${tr('errorCheckingUpdates')}\n$error",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppsRemovedNotification extends ObtainiumNotification {
|
class AppsRemovedNotification extends ObtainiumNotification {
|
||||||
@ -104,7 +128,8 @@ class AppsRemovedNotification extends ObtainiumNotification {
|
|||||||
'APPS_REMOVED',
|
'APPS_REMOVED',
|
||||||
tr('appsRemovedNotifChannel'),
|
tr('appsRemovedNotifChannel'),
|
||||||
tr('appsRemovedNotifDescription'),
|
tr('appsRemovedNotifDescription'),
|
||||||
Importance.max) {
|
Importance.max,
|
||||||
|
) {
|
||||||
message = '';
|
message = '';
|
||||||
for (var r in namedReasons) {
|
for (var r in namedReasons) {
|
||||||
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
message += '${tr('xWasRemovedDueToErrorY', args: [r[0], r[1]])} \n';
|
||||||
@ -124,7 +149,8 @@ class DownloadNotification extends ObtainiumNotification {
|
|||||||
tr('downloadNotifDescription'),
|
tr('downloadNotifDescription'),
|
||||||
Importance.low,
|
Importance.low,
|
||||||
onlyAlertOnce: true,
|
onlyAlertOnce: true,
|
||||||
progPercent: progPercent);
|
progPercent: progPercent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadedNotification extends ObtainiumNotification {
|
class DownloadedNotification extends ObtainiumNotification {
|
||||||
@ -136,7 +162,8 @@ class DownloadedNotification extends ObtainiumNotification {
|
|||||||
'FILE_DOWNLOADED',
|
'FILE_DOWNLOADED',
|
||||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||||
tr('downloadedX', args: [tr('app')]),
|
tr('downloadedX', args: [tr('app')]),
|
||||||
Importance.defaultImportance);
|
Importance.defaultImportance,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final completeInstallationNotification = ObtainiumNotification(
|
final completeInstallationNotification = ObtainiumNotification(
|
||||||
@ -146,7 +173,8 @@ final completeInstallationNotification = ObtainiumNotification(
|
|||||||
'COMPLETE_INSTALL',
|
'COMPLETE_INSTALL',
|
||||||
tr('completeAppInstallationNotifChannel'),
|
tr('completeAppInstallationNotifChannel'),
|
||||||
tr('completeAppInstallationNotifDescription'),
|
tr('completeAppInstallationNotifDescription'),
|
||||||
Importance.max);
|
Importance.max,
|
||||||
|
);
|
||||||
|
|
||||||
class CheckingUpdatesNotification extends ObtainiumNotification {
|
class CheckingUpdatesNotification extends ObtainiumNotification {
|
||||||
CheckingUpdatesNotification(String appName)
|
CheckingUpdatesNotification(String appName)
|
||||||
@ -157,7 +185,8 @@ class CheckingUpdatesNotification extends ObtainiumNotification {
|
|||||||
'BG_UPDATE_CHECK',
|
'BG_UPDATE_CHECK',
|
||||||
tr('checkingForUpdatesNotifChannel'),
|
tr('checkingForUpdatesNotifChannel'),
|
||||||
tr('checkingForUpdatesNotifDescription'),
|
tr('checkingForUpdatesNotifDescription'),
|
||||||
Importance.min);
|
Importance.min,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationsProvider {
|
class NotificationsProvider {
|
||||||
@ -173,13 +202,15 @@ class NotificationsProvider {
|
|||||||
Importance.max: Priority.max,
|
Importance.max: Priority.max,
|
||||||
Importance.min: Priority.min,
|
Importance.min: Priority.min,
|
||||||
Importance.none: Priority.min,
|
Importance.none: Priority.min,
|
||||||
Importance.unspecified: Priority.defaultPriority
|
Importance.unspecified: Priority.defaultPriority,
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
isInitialized = await notifications.initialize(
|
isInitialized =
|
||||||
|
await notifications.initialize(
|
||||||
const InitializationSettings(
|
const InitializationSettings(
|
||||||
android: AndroidInitializationSettings('ic_notification')),
|
android: AndroidInitializationSettings('ic_notification'),
|
||||||
|
),
|
||||||
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
onDidReceiveNotificationResponse: (NotificationResponse response) {
|
||||||
_showNotificationPayload(response.payload);
|
_showNotificationPayload(response.payload);
|
||||||
},
|
},
|
||||||
@ -187,16 +218,18 @@ class NotificationsProvider {
|
|||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLaunchByNotif() async {
|
Future<void> checkLaunchByNotif() async {
|
||||||
final NotificationAppLaunchDetails? launchDetails =
|
final NotificationAppLaunchDetails? launchDetails = await notifications
|
||||||
await notifications.getNotificationAppLaunchDetails();
|
.getNotificationAppLaunchDetails();
|
||||||
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
_showNotificationPayload(launchDetails!.notificationResponse?.payload,
|
_showNotificationPayload(
|
||||||
doublePop: true);
|
launchDetails!.notificationResponse?.payload,
|
||||||
|
doublePop: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNotificationPayload(String? payload, {bool doublePop = false}) {
|
void _showNotificationPayload(String? payload, {bool doublePop = false}) {
|
||||||
if (payload?.isNotEmpty == true) {
|
if (payload?.isNotEmpty == true) {
|
||||||
var title = (payload ?? '\n\n').split('\n').first;
|
var title = (payload ?? '\n\n').split('\n').first;
|
||||||
var content = (payload ?? '\n\n').split('\n').sublist(1).join('\n');
|
var content = (payload ?? '\n\n').split('\n').sublist(1).join('\n');
|
||||||
@ -213,7 +246,8 @@ class NotificationsProvider {
|
|||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(tr('ok'))),
|
child: Text(tr('ok')),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -235,11 +269,12 @@ class NotificationsProvider {
|
|||||||
String channelCode,
|
String channelCode,
|
||||||
String channelName,
|
String channelName,
|
||||||
String channelDescription,
|
String channelDescription,
|
||||||
Importance importance,
|
Importance importance, {
|
||||||
{bool cancelExisting = false,
|
bool cancelExisting = false,
|
||||||
int? progPercent,
|
int? progPercent,
|
||||||
bool onlyAlertOnce = false,
|
bool onlyAlertOnce = false,
|
||||||
String? payload}) async {
|
String? payload,
|
||||||
|
}) async {
|
||||||
if (cancelExisting) {
|
if (cancelExisting) {
|
||||||
await cancel(id);
|
await cancel(id);
|
||||||
}
|
}
|
||||||
@ -251,7 +286,9 @@ class NotificationsProvider {
|
|||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
NotificationDetails(
|
NotificationDetails(
|
||||||
android: AndroidNotificationDetails(channelCode, channelName,
|
android: AndroidNotificationDetails(
|
||||||
|
channelCode,
|
||||||
|
channelName,
|
||||||
channelDescription: channelDescription,
|
channelDescription: channelDescription,
|
||||||
importance: importance,
|
importance: importance,
|
||||||
priority: importanceToPriority[importance]!,
|
priority: importanceToPriority[importance]!,
|
||||||
@ -260,16 +297,27 @@ class NotificationsProvider {
|
|||||||
maxProgress: 100,
|
maxProgress: 100,
|
||||||
showProgress: progPercent != null,
|
showProgress: progPercent != null,
|
||||||
onlyAlertOnce: onlyAlertOnce,
|
onlyAlertOnce: onlyAlertOnce,
|
||||||
indeterminate: progPercent != null && progPercent < 0)),
|
indeterminate: progPercent != null && progPercent < 0,
|
||||||
payload: payload);
|
),
|
||||||
|
),
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notify(ObtainiumNotification notif,
|
Future<void> notify(
|
||||||
{bool cancelExisting = false}) =>
|
ObtainiumNotification notif, {
|
||||||
notifyRaw(notif.id, notif.title, notif.message, notif.channelCode,
|
bool cancelExisting = false,
|
||||||
notif.channelName, notif.channelDescription, notif.importance,
|
}) => notifyRaw(
|
||||||
|
notif.id,
|
||||||
|
notif.title,
|
||||||
|
notif.message,
|
||||||
|
notif.channelCode,
|
||||||
|
notif.channelName,
|
||||||
|
notif.channelDescription,
|
||||||
|
notif.importance,
|
||||||
cancelExisting: cancelExisting,
|
cancelExisting: cancelExisting,
|
||||||
onlyAlertOnce: notif.onlyAlertOnce,
|
onlyAlertOnce: notif.onlyAlertOnce,
|
||||||
progPercent: notif.progPercent,
|
progPercent: notif.progPercent,
|
||||||
payload: notif.payload);
|
payload: notif.payload,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,8 +58,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeSettings get theme {
|
ThemeSettings get theme {
|
||||||
return ThemeSettings
|
return ThemeSettings.values[prefs?.getInt('theme') ??
|
||||||
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
|
ThemeSettings.system.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set theme(ThemeSettings t) {
|
set theme(ThemeSettings t) {
|
||||||
@ -123,8 +123,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SortColumnSettings get sortColumn {
|
SortColumnSettings get sortColumn {
|
||||||
return SortColumnSettings.values[
|
return SortColumnSettings.values[prefs?.getInt('sortColumn') ??
|
||||||
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
SortColumnSettings.nameAuthor.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set sortColumn(SortColumnSettings s) {
|
set sortColumn(SortColumnSettings s) {
|
||||||
@ -133,8 +133,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SortOrderSettings get sortOrder {
|
SortOrderSettings get sortOrder {
|
||||||
return SortOrderSettings.values[
|
return SortOrderSettings.values[prefs?.getInt('sortOrder') ??
|
||||||
prefs?.getInt('sortOrder') ?? SortOrderSettings.ascending.index];
|
SortOrderSettings.ascending.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
set sortOrder(SortOrderSettings s) {
|
set sortOrder(SortOrderSettings s) {
|
||||||
@ -171,7 +171,9 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||||
// Explicit request as InstallPlugin request sometimes bugged
|
// Explicit request as InstallPlugin request sometimes bugged
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
msg: tr('pleaseAllowInstallPerm'),
|
||||||
|
toastLength: Toast.LENGTH_LONG,
|
||||||
|
);
|
||||||
if ((await Permission.requestInstallPackages.request()) ==
|
if ((await Permission.requestInstallPackages.request()) ==
|
||||||
PermissionStatus.granted) {
|
PermissionStatus.granted) {
|
||||||
return true;
|
return true;
|
||||||
@ -470,7 +472,8 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> get searchDeselected {
|
List<String> 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<String> list) {
|
set searchDeselected(List<String> list) {
|
||||||
|
@ -53,29 +53,40 @@ class APKDetails {
|
|||||||
late String? changeLog;
|
late String? changeLog;
|
||||||
late List<MapEntry<String, String>> allAssetUrls;
|
late List<MapEntry<String, String>> allAssetUrls;
|
||||||
|
|
||||||
APKDetails(this.version, this.apkUrls, this.names,
|
APKDetails(
|
||||||
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
|
this.version,
|
||||||
|
this.apkUrls,
|
||||||
|
this.names, {
|
||||||
|
this.releaseDate,
|
||||||
|
this.changeLog,
|
||||||
|
this.allAssetUrls = const [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
List<List<String>> stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||||
mapList.map((e) => [e.key, e.value]).toList();
|
mapList.map((e) => [e.key, e.value]).toList();
|
||||||
|
|
||||||
assumed2DlistToStringMapList(List<dynamic> arr) =>
|
List<MapEntry<String, String>> assumed2DlistToStringMapList(List<dynamic> arr) =>
|
||||||
arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
|
arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
|
||||||
|
|
||||||
// App JSON schema has changed multiple times over the many versions of Obtainium
|
// 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
|
// This function takes an App JSON and modifies it if needed to conform to the latest (current) version
|
||||||
appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
Map<String, dynamic> appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
||||||
var source = SourceProvider()
|
var source = SourceProvider().getSource(
|
||||||
.getSource(json['url'], overrideSource: json['overrideSource']);
|
json['url'],
|
||||||
var formItems = source.combinedAppSpecificSettingFormItems
|
overrideSource: json['overrideSource'],
|
||||||
.reduce((value, element) => [...value, ...element]);
|
);
|
||||||
Map<String, dynamic> additionalSettings =
|
var formItems = source.combinedAppSpecificSettingFormItems.reduce(
|
||||||
getDefaultValuesFromFormItems([formItems]);
|
(value, element) => [...value, ...element],
|
||||||
|
);
|
||||||
|
Map<String, dynamic> additionalSettings = getDefaultValuesFromFormItems([
|
||||||
|
formItems,
|
||||||
|
]);
|
||||||
Map<String, dynamic> originalAdditionalSettings = {};
|
Map<String, dynamic> originalAdditionalSettings = {};
|
||||||
if (json['additionalSettings'] != null) {
|
if (json['additionalSettings'] != null) {
|
||||||
originalAdditionalSettings =
|
originalAdditionalSettings = Map<String, dynamic>.from(
|
||||||
Map<String, dynamic>.from(jsonDecode(json['additionalSettings']));
|
jsonDecode(json['additionalSettings']),
|
||||||
|
);
|
||||||
additionalSettings.addEntries(originalAdditionalSettings.entries);
|
additionalSettings.addEntries(originalAdditionalSettings.entries);
|
||||||
}
|
}
|
||||||
// If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
|
// If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
|
||||||
@ -127,12 +138,14 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
// Ensure additionalSettings are correctly typed
|
// Ensure additionalSettings are correctly typed
|
||||||
for (var item in formItems) {
|
for (var item in formItems) {
|
||||||
if (additionalSettings[item.key] != null) {
|
if (additionalSettings[item.key] != null) {
|
||||||
additionalSettings[item.key] =
|
additionalSettings[item.key] = item.ensureType(
|
||||||
item.ensureType(additionalSettings[item.key]);
|
additionalSettings[item.key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int preferredApkIndex =
|
int preferredApkIndex = json['preferredApkIndex'] == null
|
||||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int;
|
? 0
|
||||||
|
: json['preferredApkIndex'] as int;
|
||||||
if (preferredApkIndex < 0) {
|
if (preferredApkIndex < 0) {
|
||||||
preferredApkIndex = 0;
|
preferredApkIndex = 0;
|
||||||
}
|
}
|
||||||
@ -145,9 +158,9 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
|
apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson));
|
apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson));
|
||||||
apkUrls = List<dynamic>.from(apkUrlJson)
|
apkUrls = List<dynamic>.from(
|
||||||
.map((e) => MapEntry(e[0] as String, e[1] as String))
|
apkUrlJson,
|
||||||
.toList();
|
).map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
|
||||||
}
|
}
|
||||||
json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls));
|
json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls));
|
||||||
}
|
}
|
||||||
@ -173,8 +186,8 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
'customLinkFilterRegex':
|
'customLinkFilterRegex':
|
||||||
originalAdditionalSettings['intermediateLinkRegex'],
|
originalAdditionalSettings['intermediateLinkRegex'],
|
||||||
'filterByLinkText':
|
'filterByLinkText':
|
||||||
originalAdditionalSettings['intermediateLinkByText']
|
originalAdditionalSettings['intermediateLinkByText'],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) {
|
if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) {
|
||||||
@ -188,7 +201,8 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
if (legacySteamSourceApps.contains(additionalSettings['app'] ?? '')) {
|
if (legacySteamSourceApps.contains(additionalSettings['app'] ?? '')) {
|
||||||
json['url'] = '${json['url']}/mobile';
|
json['url'] = '${json['url']}/mobile';
|
||||||
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
||||||
HTML().combinedAppSpecificSettingFormItems);
|
HTML().combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
for (var s in replacementAdditionalSettings.keys) {
|
for (var s in replacementAdditionalSettings.keys) {
|
||||||
if (additionalSettings.containsKey(s)) {
|
if (additionalSettings.containsKey(s)) {
|
||||||
replacementAdditionalSettings[s] = additionalSettings[s];
|
replacementAdditionalSettings[s] = additionalSettings[s];
|
||||||
@ -212,7 +226,8 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
json['lastUpdateCheck'] != null) {
|
json['lastUpdateCheck'] != null) {
|
||||||
json['url'] = 'https://updates.signal.org/android/latest.json';
|
json['url'] = 'https://updates.signal.org/android/latest.json';
|
||||||
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
||||||
HTML().combinedAppSpecificSettingFormItems);
|
HTML().combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
replacementAdditionalSettings['versionExtractionRegEx'] =
|
replacementAdditionalSettings['versionExtractionRegEx'] =
|
||||||
'\\d+.\\d+.\\d+';
|
'\\d+.\\d+.\\d+';
|
||||||
additionalSettings = replacementAdditionalSettings;
|
additionalSettings = replacementAdditionalSettings;
|
||||||
@ -228,7 +243,8 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
json['lastUpdateCheck'] != null) {
|
json['lastUpdateCheck'] != null) {
|
||||||
json['url'] = 'https://whatsapp.com/android';
|
json['url'] = 'https://whatsapp.com/android';
|
||||||
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
||||||
HTML().combinedAppSpecificSettingFormItems);
|
HTML().combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
replacementAdditionalSettings['refreshBeforeDownload'] = true;
|
replacementAdditionalSettings['refreshBeforeDownload'] = true;
|
||||||
additionalSettings = replacementAdditionalSettings;
|
additionalSettings = replacementAdditionalSettings;
|
||||||
}
|
}
|
||||||
@ -243,7 +259,8 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
json['lastUpdateCheck'] != null) {
|
json['lastUpdateCheck'] != null) {
|
||||||
json['url'] = 'https://www.videolan.org/vlc/download-android.html';
|
json['url'] = 'https://www.videolan.org/vlc/download-android.html';
|
||||||
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
var replacementAdditionalSettings = getDefaultValuesFromFormItems(
|
||||||
HTML().combinedAppSpecificSettingFormItems);
|
HTML().combinedAppSpecificSettingFormItems,
|
||||||
|
);
|
||||||
replacementAdditionalSettings['refreshBeforeDownload'] = true;
|
replacementAdditionalSettings['refreshBeforeDownload'] = true;
|
||||||
replacementAdditionalSettings['intermediateLink'] =
|
replacementAdditionalSettings['intermediateLink'] =
|
||||||
<Map<String, dynamic>>[
|
<Map<String, dynamic>>[
|
||||||
@ -252,15 +269,15 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
'filterByLinkText': true,
|
'filterByLinkText': true,
|
||||||
'skipSort': false,
|
'skipSort': false,
|
||||||
'reverseSort': false,
|
'reverseSort': false,
|
||||||
'sortByLastLinkSegment': false
|
'sortByLastLinkSegment': false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'customLinkFilterRegex': 'arm64-v8a\\.apk\$',
|
'customLinkFilterRegex': 'arm64-v8a\\.apk\$',
|
||||||
'filterByLinkText': false,
|
'filterByLinkText': false,
|
||||||
'skipSort': false,
|
'skipSort': false,
|
||||||
'reverseSort': false,
|
'reverseSort': false,
|
||||||
'sortByLastLinkSegment': false
|
'sortByLastLinkSegment': false,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
replacementAdditionalSettings['versionExtractionRegEx'] =
|
replacementAdditionalSettings['versionExtractionRegEx'] =
|
||||||
'/vlc-android/([^/]+)/';
|
'/vlc-android/([^/]+)/';
|
||||||
@ -277,8 +294,9 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
|||||||
json['overrideSource'] = FDroid().runtimeType.toString();
|
json['overrideSource'] = FDroid().runtimeType.toString();
|
||||||
} else if (overrideSourceWasUndefined) {
|
} else if (overrideSourceWasUndefined) {
|
||||||
// Similar to above, but for third-party F-Droid repos
|
// Similar to above, but for third-party F-Droid repos
|
||||||
RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)')
|
RegExpMatch? match = RegExp(
|
||||||
.firstMatch(json['url'] as String);
|
'^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)',
|
||||||
|
).firstMatch(json['url'] as String);
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
json['overrideSource'] = FDroidRepo().runtimeType.toString();
|
json['overrideSource'] = FDroidRepo().runtimeType.toString();
|
||||||
}
|
}
|
||||||
@ -315,13 +333,14 @@ class App {
|
|||||||
this.preferredApkIndex,
|
this.preferredApkIndex,
|
||||||
this.additionalSettings,
|
this.additionalSettings,
|
||||||
this.lastUpdateCheck,
|
this.lastUpdateCheck,
|
||||||
this.pinned,
|
this.pinned, {
|
||||||
{this.categories = const [],
|
this.categories = const [],
|
||||||
this.releaseDate,
|
this.releaseDate,
|
||||||
this.changeLog,
|
this.changeLog,
|
||||||
this.overrideSource,
|
this.overrideSource,
|
||||||
this.allowIdChange = false,
|
this.allowIdChange = false,
|
||||||
this.otherAssetUrls = const []});
|
this.otherAssetUrls = const [],
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -363,7 +382,8 @@ class App {
|
|||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
overrideSource: overrideSource,
|
overrideSource: overrideSource,
|
||||||
allowIdChange: allowIdChange,
|
allowIdChange: allowIdChange,
|
||||||
otherAssetUrls: otherAssetUrls);
|
otherAssetUrls: otherAssetUrls,
|
||||||
|
);
|
||||||
|
|
||||||
factory App.fromJson(Map<String, dynamic> json) {
|
factory App.fromJson(Map<String, dynamic> json) {
|
||||||
Map<String, dynamic> originalJSON = Map.from(json);
|
Map<String, dynamic> originalJSON = Map.from(json);
|
||||||
@ -372,7 +392,8 @@ class App {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
json = originalJSON;
|
json = originalJSON;
|
||||||
LogsProvider().add(
|
LogsProvider().add(
|
||||||
'Error running JSON compat modifiers: ${e.toString()}: ${originalJSON.toString()}');
|
'Error running JSON compat modifiers: ${e.toString()}: ${originalJSON.toString()}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return App(
|
return App(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
@ -384,7 +405,8 @@ class App {
|
|||||||
: json['installedVersion'] as String,
|
: json['installedVersion'] as String,
|
||||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||||
assumed2DlistToStringMapList(
|
assumed2DlistToStringMapList(
|
||||||
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]')),
|
||||||
|
),
|
||||||
(json['preferredApkIndex'] ?? -1) as int,
|
(json['preferredApkIndex'] ?? -1) as int,
|
||||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
@ -405,7 +427,8 @@ class App {
|
|||||||
overrideSource: json['overrideSource'],
|
overrideSource: json['overrideSource'],
|
||||||
allowIdChange: json['allowIdChange'] ?? false,
|
allowIdChange: json['allowIdChange'] ?? false,
|
||||||
otherAssetUrls: assumed2DlistToStringMapList(
|
otherAssetUrls: assumed2DlistToStringMapList(
|
||||||
jsonDecode((json['otherAssetUrls'] ?? '[]'))),
|
jsonDecode((json['otherAssetUrls'] ?? '[]')),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,12 +449,12 @@ class App {
|
|||||||
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
||||||
'changeLog': changeLog,
|
'changeLog': changeLog,
|
||||||
'overrideSource': overrideSource,
|
'overrideSource': overrideSource,
|
||||||
'allowIdChange': allowIdChange
|
'allowIdChange': allowIdChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the input is starts with HTTPS and has no WWW
|
// Ensure the input is starts with HTTPS and has no WWW
|
||||||
preStandardizeUrl(String url) {
|
String preStandardizeUrl(String url) {
|
||||||
var firstDotIndex = url.indexOf('.');
|
var firstDotIndex = url.indexOf('.');
|
||||||
if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) {
|
if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) {
|
||||||
throw UnsupportedURLError();
|
throw UnsupportedURLError();
|
||||||
@ -441,11 +464,13 @@ preStandardizeUrl(String url) {
|
|||||||
url = 'https://$url';
|
url = 'https://$url';
|
||||||
}
|
}
|
||||||
var uri = Uri.tryParse(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?.path.isEmpty ?? false) && url.endsWith('/'))) &&
|
||||||
(uri?.queryParameters.isEmpty ?? false);
|
(uri?.queryParameters.isEmpty ?? false);
|
||||||
|
|
||||||
url = url
|
url =
|
||||||
|
url
|
||||||
.split('/')
|
.split('/')
|
||||||
.where((e) => e.isNotEmpty)
|
.where((e) => e.isNotEmpty)
|
||||||
.join('/')
|
.join('/')
|
||||||
@ -457,8 +482,10 @@ preStandardizeUrl(String url) {
|
|||||||
String noAPKFound = tr('noAPKFound');
|
String noAPKFound = tr('noAPKFound');
|
||||||
|
|
||||||
List<String> getLinksFromParsedHTML(
|
List<String> getLinksFromParsedHTML(
|
||||||
Document dom, RegExp hrefPattern, String prependToLinks) =>
|
Document dom,
|
||||||
dom
|
RegExp hrefPattern,
|
||||||
|
String prependToLinks,
|
||||||
|
) => dom
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.where((element) {
|
.where((element) {
|
||||||
if (element.attributes['href'] == null) return false;
|
if (element.attributes['href'] == null) return false;
|
||||||
@ -468,10 +495,13 @@ List<String> getLinksFromParsedHTML(
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Map<String, dynamic> getDefaultValuesFromFormItems(
|
Map<String, dynamic> getDefaultValuesFromFormItems(
|
||||||
List<List<GeneratedFormItem>> items) {
|
List<List<GeneratedFormItem>> items,
|
||||||
return Map.fromEntries(items
|
) {
|
||||||
|
return Map.fromEntries(
|
||||||
|
items
|
||||||
.map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? '')))
|
.map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? '')))
|
||||||
.reduce((value, element) => [...value, ...element]));
|
.reduce((value, element) => [...value, ...element]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
||||||
@ -482,7 +512,8 @@ List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
Future<List<MapEntry<String, String>>> filterApksByArch(
|
Future<List<MapEntry<String, String>>> filterApksByArch(
|
||||||
List<MapEntry<String, String>> apkUrls) async {
|
List<MapEntry<String, String>> apkUrls,
|
||||||
|
) async {
|
||||||
if (apkUrls.length > 1) {
|
if (apkUrls.length > 1) {
|
||||||
var abis = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
var abis = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
for (var abi in abis) {
|
for (var abi in abis) {
|
||||||
@ -498,7 +529,7 @@ Future<List<MapEntry<String, String>>> filterApksByArch(
|
|||||||
return apkUrls;
|
return apkUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSourceRegex(List<String> hosts) {
|
String getSourceRegex(List<String> hosts) {
|
||||||
return '(${hosts.join('|').replaceAll('.', '\\.')})';
|
return '(${hosts.join('|').replaceAll('.', '\\.')})';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,20 +542,23 @@ HttpClient createHttpClient(bool insecure) {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MapEntry<HttpClient, HttpClientResponse>> sourceRequestStreamResponse(
|
Future<MapEntry<Uri, MapEntry<HttpClient, HttpClientResponse>>>
|
||||||
|
sourceRequestStreamResponse(
|
||||||
String method,
|
String method,
|
||||||
String url,
|
String url,
|
||||||
Map<String, String>? requestHeaders,
|
Map<String, String>? requestHeaders,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool followRedirects = true,
|
bool followRedirects = true,
|
||||||
Object? postBody}) async {
|
Object? postBody,
|
||||||
|
}) async {
|
||||||
var currentUrl = Uri.parse(url);
|
var currentUrl = Uri.parse(url);
|
||||||
var redirectCount = 0;
|
var redirectCount = 0;
|
||||||
const maxRedirects = 10;
|
const maxRedirects = 10;
|
||||||
List<Cookie> cookies = [];
|
List<Cookie> cookies = [];
|
||||||
while (redirectCount < maxRedirects) {
|
while (redirectCount < maxRedirects) {
|
||||||
var httpClient =
|
var httpClient = createHttpClient(
|
||||||
createHttpClient(additionalSettings['allowInsecure'] == true);
|
additionalSettings['allowInsecure'] == true,
|
||||||
|
);
|
||||||
var request = await httpClient.openUrl(method, currentUrl);
|
var request = await httpClient.openUrl(method, currentUrl);
|
||||||
if (requestHeaders != null) {
|
if (requestHeaders != null) {
|
||||||
requestHeaders.forEach((key, value) {
|
requestHeaders.forEach((key, value) {
|
||||||
@ -551,16 +585,21 @@ Future<MapEntry<HttpClient, HttpClientResponse>> sourceRequestStreamResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapEntry(httpClient, response);
|
return MapEntry(currentUrl, MapEntry(httpClient, response));
|
||||||
}
|
}
|
||||||
throw ObtainiumError('Too many redirects ($maxRedirects)');
|
throw ObtainiumError('Too many redirects ($maxRedirects)');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> httpClientResponseStreamToFinalResponse(HttpClient httpClient,
|
Future<Response> httpClientResponseStreamToFinalResponse(
|
||||||
String method, String url, HttpClientResponse response) async {
|
HttpClient httpClient,
|
||||||
final bytes =
|
String method,
|
||||||
(await response.fold<BytesBuilder>(BytesBuilder(), (b, d) => b..add(d)))
|
String url,
|
||||||
.toBytes();
|
HttpClientResponse response,
|
||||||
|
) async {
|
||||||
|
final bytes = (await response.fold<BytesBuilder>(
|
||||||
|
BytesBuilder(),
|
||||||
|
(b, d) => b..add(d),
|
||||||
|
)).toBytes();
|
||||||
|
|
||||||
final headers = <String, String>{};
|
final headers = <String, String>{};
|
||||||
response.headers.forEach((name, values) {
|
response.headers.forEach((name, values) {
|
||||||
@ -598,11 +637,14 @@ abstract class AppSource {
|
|||||||
name = runtimeType.toString();
|
name = runtimeType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(String key,
|
void overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
||||||
{bool disabled = true, bool defaultValue = true}) {
|
String key, {
|
||||||
|
bool disabled = true,
|
||||||
|
bool defaultValue = true,
|
||||||
|
}) {
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.map(
|
||||||
.map((e) {
|
(e) {
|
||||||
return e.map((e2) {
|
return e.map((e2) {
|
||||||
if (e2.key == key) {
|
if (e2.key == key) {
|
||||||
var item = e2 as GeneratedFormSwitch;
|
var item = e2 as GeneratedFormSwitch;
|
||||||
@ -611,7 +653,8 @@ abstract class AppSource {
|
|||||||
}
|
}
|
||||||
return e2;
|
return e2;
|
||||||
}).toList();
|
}).toList();
|
||||||
}).toList();
|
},
|
||||||
|
).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
String standardizeUrl(String url) {
|
String standardizeUrl(String url) {
|
||||||
@ -623,8 +666,9 @@ abstract class AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>?> getRequestHeaders(
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings, {
|
||||||
{bool forAPKDownload = false}) async {
|
bool forAPKDownload = false,
|
||||||
|
}) async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,18 +677,28 @@ abstract class AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> sourceRequest(
|
Future<Response> sourceRequest(
|
||||||
String url, Map<String, dynamic> additionalSettings,
|
String url,
|
||||||
{bool followRedirects = true, Object? postBody}) async {
|
Map<String, dynamic> additionalSettings, {
|
||||||
|
bool followRedirects = true,
|
||||||
|
Object? postBody,
|
||||||
|
}) async {
|
||||||
var method = postBody == null ? 'GET' : 'POST';
|
var method = postBody == null ? 'GET' : 'POST';
|
||||||
var requestHeaders = await getRequestHeaders(additionalSettings);
|
var requestHeaders = await getRequestHeaders(additionalSettings);
|
||||||
var streamedResponseAndClient = await sourceRequestStreamResponse(
|
var streamedResponseUrlWithResponseAndClient =
|
||||||
method, url, requestHeaders, additionalSettings,
|
await sourceRequestStreamResponse(
|
||||||
followRedirects: followRedirects, postBody: postBody);
|
|
||||||
return await httpClientResponseStreamToFinalResponse(
|
|
||||||
streamedResponseAndClient.key,
|
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
streamedResponseAndClient.value);
|
requestHeaders,
|
||||||
|
additionalSettings,
|
||||||
|
followRedirects: followRedirects,
|
||||||
|
postBody: postBody,
|
||||||
|
);
|
||||||
|
return await httpClientResponseStreamToFinalResponse(
|
||||||
|
streamedResponseUrlWithResponseAndClient.value.key,
|
||||||
|
method,
|
||||||
|
streamedResponseUrlWithResponseAndClient.key.toString(),
|
||||||
|
streamedResponseUrlWithResponseAndClient.value.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void runOnAddAppInputChange(String inputUrl) {
|
void runOnAddAppInputChange(String inputUrl) {
|
||||||
@ -656,7 +710,9 @@ abstract class AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) {
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -667,120 +723,159 @@ abstract class AppSource {
|
|||||||
// Some additional data may be needed for Apps regardless of Source
|
// Some additional data may be needed for Apps regardless of Source
|
||||||
List<List<GeneratedFormItem>>
|
List<List<GeneratedFormItem>>
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly = [
|
||||||
|
[GeneratedFormSwitch('trackOnly', label: tr('trackOnly'))],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch(
|
GeneratedFormTextField(
|
||||||
'trackOnly',
|
'versionExtractionRegEx',
|
||||||
label: tr('trackOnly'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
[
|
|
||||||
GeneratedFormTextField('versionExtractionRegEx',
|
|
||||||
label: tr('trimVersionString'),
|
label: tr('trimVersionString'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [(value) => regExValidator(value)]),
|
additionalValidators: [(value) => regExValidator(value)],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('matchGroupToUse',
|
GeneratedFormTextField(
|
||||||
|
'matchGroupToUse',
|
||||||
label: tr('matchGroupToUseForX', args: [tr('trimVersionString')]),
|
label: tr('matchGroupToUseForX', args: [tr('trimVersionString')]),
|
||||||
required: false,
|
required: false,
|
||||||
hint: '\$0')
|
hint: '\$0',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('versionDetection',
|
GeneratedFormSwitch(
|
||||||
label: tr('versionDetectionExplanation'), defaultValue: true)
|
'versionDetection',
|
||||||
|
label: tr('versionDetectionExplanation'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('useVersionCodeAsOSVersion',
|
GeneratedFormSwitch(
|
||||||
label: tr('useVersionCodeAsOSVersion'), defaultValue: false)
|
'useVersionCodeAsOSVersion',
|
||||||
|
label: tr('useVersionCodeAsOSVersion'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormTextField('apkFilterRegEx',
|
GeneratedFormTextField(
|
||||||
|
'apkFilterRegEx',
|
||||||
label: tr('filterAPKsByRegEx'),
|
label: tr('filterAPKsByRegEx'),
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
},
|
||||||
])
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('invertAPKFilter',
|
GeneratedFormSwitch(
|
||||||
|
'invertAPKFilter',
|
||||||
label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})',
|
label: '${tr('invertRegEx')} (${tr('filterAPKsByRegEx')})',
|
||||||
defaultValue: false)
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('autoApkFilterByArch',
|
GeneratedFormSwitch(
|
||||||
label: tr('autoApkFilterByArch'), defaultValue: true)
|
'autoApkFilterByArch',
|
||||||
|
label: tr('autoApkFilterByArch'),
|
||||||
|
defaultValue: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
||||||
[GeneratedFormTextField('appAuthor', label: tr('author'), required: false)],
|
[GeneratedFormTextField('appAuthor', label: tr('author'), required: false)],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('shizukuPretendToBeGooglePlay',
|
GeneratedFormSwitch(
|
||||||
label: tr('shizukuPretendToBeGooglePlay'), defaultValue: false)
|
'shizukuPretendToBeGooglePlay',
|
||||||
|
label: tr('shizukuPretendToBeGooglePlay'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('allowInsecure',
|
GeneratedFormSwitch(
|
||||||
label: tr('allowInsecure'), defaultValue: false)
|
'allowInsecure',
|
||||||
|
label: tr('allowInsecure'),
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('exemptFromBackgroundUpdates',
|
GeneratedFormSwitch(
|
||||||
label: tr('exemptFromBackgroundUpdates'))
|
'exemptFromBackgroundUpdates',
|
||||||
|
label: tr('exemptFromBackgroundUpdates'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('skipUpdateNotifications',
|
GeneratedFormSwitch(
|
||||||
label: tr('skipUpdateNotifications'))
|
'skipUpdateNotifications',
|
||||||
|
label: tr('skipUpdateNotifications'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[GeneratedFormTextField('about', label: tr('about'), required: false)],
|
[GeneratedFormTextField('about', label: tr('about'), required: false)],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('refreshBeforeDownload',
|
GeneratedFormSwitch(
|
||||||
label: tr('refreshBeforeDownload'))
|
'refreshBeforeDownload',
|
||||||
]
|
label: tr('refreshBeforeDownload'),
|
||||||
|
),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous 2 variables combined into one at runtime for convenient usage
|
// Previous 2 variables combined into one at runtime for convenient usage
|
||||||
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
|
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
|
||||||
if (showReleaseDateAsVersionToggle == true) {
|
if (showReleaseDateAsVersionToggle == true) {
|
||||||
if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
if (additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||||
.indexWhere((List<GeneratedFormItem> e) =>
|
.indexWhere(
|
||||||
e.indexWhere((GeneratedFormItem i) =>
|
(List<GeneratedFormItem> e) =>
|
||||||
i.key == 'releaseDateAsVersion') >=
|
e.indexWhere(
|
||||||
0) <
|
(GeneratedFormItem i) => i.key == 'releaseDateAsVersion',
|
||||||
|
) >=
|
||||||
|
0,
|
||||||
|
) <
|
||||||
0) {
|
0) {
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly.insert(
|
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||||
.indexWhere((List<GeneratedFormItem> e) =>
|
.insert(
|
||||||
e.indexWhere((GeneratedFormItem i) =>
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||||
i.key == 'versionDetection') >=
|
.indexWhere(
|
||||||
0) +
|
(List<GeneratedFormItem> e) =>
|
||||||
|
e.indexWhere(
|
||||||
|
(GeneratedFormItem i) =>
|
||||||
|
i.key == 'versionDetection',
|
||||||
|
) >=
|
||||||
|
0,
|
||||||
|
) +
|
||||||
1,
|
1,
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('releaseDateAsVersion',
|
GeneratedFormSwitch(
|
||||||
|
'releaseDateAsVersion',
|
||||||
label:
|
label:
|
||||||
'${tr('releaseDateAsVersion')} (${tr('pseudoVersion')})',
|
'${tr('releaseDateAsVersion')} (${tr('pseudoVersion')})',
|
||||||
defaultValue: false)
|
defaultValue: false,
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly =
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
||||||
.map((e) => e
|
.map(
|
||||||
|
(e) => e
|
||||||
.where((ee) => !excludeCommonSettingKeys.contains(ee.key))
|
.where((ee) => !excludeCommonSettingKeys.contains(ee.key))
|
||||||
.toList())
|
.toList(),
|
||||||
|
)
|
||||||
.where((e) => e.isNotEmpty)
|
.where((e) => e.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
if (versionDetectionDisallowed) {
|
if (versionDetectionDisallowed) {
|
||||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
||||||
'versionDetection',
|
'versionDetection',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
defaultValue: false);
|
defaultValue: false,
|
||||||
|
);
|
||||||
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
overrideAdditionalAppSpecificSourceAgnosticSettingSwitch(
|
||||||
'useVersionCodeAsOSVersion',
|
'useVersionCodeAsOSVersion',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
defaultValue: false);
|
defaultValue: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
...additionalSourceAppSpecificSettingFormItems,
|
...additionalSourceAppSpecificSettingFormItems,
|
||||||
...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly
|
...additionalAppSpecificSourceAgnosticSettingFormItemsNeverUseDirectly,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -789,7 +884,8 @@ abstract class AppSource {
|
|||||||
List<GeneratedFormItem> sourceConfigSettingFormItems = [];
|
List<GeneratedFormItem> sourceConfigSettingFormItems = [];
|
||||||
Future<Map<String, String>> getSourceConfigValues(
|
Future<Map<String, String>> getSourceConfigValues(
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
SettingsProvider settingsProvider) async {
|
SettingsProvider settingsProvider,
|
||||||
|
) async {
|
||||||
Map<String, String> results = {};
|
Map<String, String> results = {};
|
||||||
for (var e in sourceConfigSettingFormItems) {
|
for (var e in sourceConfigSettingFormItems) {
|
||||||
var val = hostChanged && !hostIdenticalDespiteAnyChange
|
var val = hostChanged && !hostIdenticalDespiteAnyChange
|
||||||
@ -811,31 +907,40 @@ abstract class AppSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl, String standardUrl,
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Map<String, dynamic> additionalSettings) async {
|
String apkUrl,
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canSearch = false;
|
bool canSearch = false;
|
||||||
bool includeAdditionalOptsInMainSearch = false;
|
bool includeAdditionalOptsInMainSearch = false;
|
||||||
List<GeneratedFormItem> searchQuerySettingFormItems = [];
|
List<GeneratedFormItem> searchQuerySettingFormItems = [];
|
||||||
Future<Map<String, List<String>>> search(String query,
|
Future<Map<String, List<String>>> search(
|
||||||
{Map<String, dynamic> querySettings = const {}}) {
|
String query, {
|
||||||
|
Map<String, dynamic> querySettings = const {},
|
||||||
|
}) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
String standardUrl, {
|
||||||
|
Map<String, dynamic> additionalSettings = const {},
|
||||||
|
}) async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ObtainiumError getObtainiumHttpError(Response res) {
|
ObtainiumError getObtainiumHttpError(Response res) {
|
||||||
return ObtainiumError((res.reasonPhrase != null &&
|
return ObtainiumError(
|
||||||
|
(res.reasonPhrase != null &&
|
||||||
res.reasonPhrase != null &&
|
res.reasonPhrase != null &&
|
||||||
res.reasonPhrase!.isNotEmpty)
|
res.reasonPhrase!.isNotEmpty)
|
||||||
? res.reasonPhrase!
|
? res.reasonPhrase!
|
||||||
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
|
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MassAppUrlSource {
|
abstract class MassAppUrlSource {
|
||||||
@ -844,7 +949,7 @@ abstract class MassAppUrlSource {
|
|||||||
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
regExValidator(String? value) {
|
String? regExValidator(String? value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -856,7 +961,7 @@ regExValidator(String? value) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
intValidator(String? value, {bool positive = false}) {
|
String? intValidator(String? value, {bool positive = false}) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return tr('invalidInput');
|
return tr('invalidInput');
|
||||||
}
|
}
|
||||||
@ -875,7 +980,7 @@ bool isTempId(App app) {
|
|||||||
return RegExp('^[0-9]+\$').hasMatch(app.id);
|
return RegExp('^[0-9]+\$').hasMatch(app.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) {
|
String? replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) {
|
||||||
if (RegExp('^\\d+\$').hasMatch(matchGroupString)) {
|
if (RegExp('^\\d+\$').hasMatch(matchGroupString)) {
|
||||||
matchGroupString = '\$$matchGroupString';
|
matchGroupString = '\$$matchGroupString';
|
||||||
}
|
}
|
||||||
@ -904,8 +1009,11 @@ replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) {
|
|||||||
return outputString;
|
return outputString;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? extractVersion(String? versionExtractionRegEx, String? matchGroupString,
|
String? extractVersion(
|
||||||
String stringToCheck) {
|
String? versionExtractionRegEx,
|
||||||
|
String? matchGroupString,
|
||||||
|
String stringToCheck,
|
||||||
|
) {
|
||||||
if (versionExtractionRegEx?.isNotEmpty == true) {
|
if (versionExtractionRegEx?.isNotEmpty == true) {
|
||||||
String? version = stringToCheck;
|
String? version = stringToCheck;
|
||||||
var match = RegExp(versionExtractionRegEx!).allMatches(version);
|
var match = RegExp(versionExtractionRegEx!).allMatches(version);
|
||||||
@ -929,7 +1037,8 @@ String? extractVersion(String? versionExtractionRegEx, String? matchGroupString,
|
|||||||
List<MapEntry<String, String>> filterApks(
|
List<MapEntry<String, String>> filterApks(
|
||||||
List<MapEntry<String, String>> apkUrls,
|
List<MapEntry<String, String>> apkUrls,
|
||||||
String? apkFilterRegEx,
|
String? apkFilterRegEx,
|
||||||
bool? invert) {
|
bool? invert,
|
||||||
|
) {
|
||||||
if (apkFilterRegEx?.isNotEmpty == true) {
|
if (apkFilterRegEx?.isNotEmpty == true) {
|
||||||
var reg = RegExp(apkFilterRegEx!);
|
var reg = RegExp(apkFilterRegEx!);
|
||||||
apkUrls = apkUrls.where((element) {
|
apkUrls = apkUrls.where((element) {
|
||||||
@ -940,7 +1049,7 @@ List<MapEntry<String, String>> filterApks(
|
|||||||
return apkUrls;
|
return apkUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
isVersionPseudo(App app) =>
|
bool isVersionPseudo(App app) =>
|
||||||
app.additionalSettings['trackOnly'] == true ||
|
app.additionalSettings['trackOnly'] == true ||
|
||||||
(app.installedVersion != null &&
|
(app.installedVersion != null &&
|
||||||
app.additionalSettings['versionDetection'] != true);
|
app.additionalSettings['versionDetection'] != true);
|
||||||
@ -968,7 +1077,7 @@ class SourceProvider {
|
|||||||
TelegramApp(),
|
TelegramApp(),
|
||||||
NeutronCode(),
|
NeutronCode(),
|
||||||
DirectAPKLink(),
|
DirectAPKLink(),
|
||||||
HTML() // This should ALWAYS be the last option as they are tried in order
|
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
|
// Add more mass url source classes here so they are available via the service
|
||||||
@ -977,8 +1086,9 @@ class SourceProvider {
|
|||||||
AppSource getSource(String url, {String? overrideSource}) {
|
AppSource getSource(String url, {String? overrideSource}) {
|
||||||
url = preStandardizeUrl(url);
|
url = preStandardizeUrl(url);
|
||||||
if (overrideSource != null) {
|
if (overrideSource != null) {
|
||||||
var srcs =
|
var srcs = sources.where(
|
||||||
sources.where((e) => e.runtimeType.toString() == overrideSource);
|
(e) => e.runtimeType.toString() == overrideSource,
|
||||||
|
);
|
||||||
if (srcs.isEmpty) {
|
if (srcs.isEmpty) {
|
||||||
throw UnsupportedURLError();
|
throw UnsupportedURLError();
|
||||||
}
|
}
|
||||||
@ -996,8 +1106,8 @@ class SourceProvider {
|
|||||||
for (var s in sources.where((element) => element.hosts.isNotEmpty)) {
|
for (var s in sources.where((element) => element.hosts.isNotEmpty)) {
|
||||||
try {
|
try {
|
||||||
if (RegExp(
|
if (RegExp(
|
||||||
'^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$')
|
'^${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}(${getSourceRegex(s.hosts)})\$',
|
||||||
.hasMatch(Uri.parse(url).host)) {
|
).hasMatch(Uri.parse(url).host)) {
|
||||||
source = s;
|
source = s;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1007,7 +1117,8 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
for (var s in sources.where(
|
for (var s in sources.where(
|
||||||
(element) => element.hosts.isEmpty && !element.neverAutoSelect)) {
|
(element) => element.hosts.isEmpty && !element.neverAutoSelect,
|
||||||
|
)) {
|
||||||
try {
|
try {
|
||||||
s.sourceSpecificStandardizeURL(url, forSelection: true);
|
s.sourceSpecificStandardizeURL(url, forSelection: true);
|
||||||
source = s;
|
source = s;
|
||||||
@ -1035,22 +1146,28 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String generateTempID(
|
String generateTempID(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) =>
|
String standardUrl,
|
||||||
(standardUrl + additionalSettings.toString()).hashCode.toString();
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) => (standardUrl + additionalSettings.toString()).hashCode.toString();
|
||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(
|
||||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
AppSource source,
|
||||||
{App? currentApp,
|
String url,
|
||||||
|
Map<String, dynamic> additionalSettings, {
|
||||||
|
App? currentApp,
|
||||||
bool trackOnlyOverride = false,
|
bool trackOnlyOverride = false,
|
||||||
bool sourceIsOverriden = false,
|
bool sourceIsOverriden = false,
|
||||||
bool inferAppIdIfOptional = false}) async {
|
bool inferAppIdIfOptional = false,
|
||||||
|
}) async {
|
||||||
if (trackOnlyOverride || source.enforceTrackOnly) {
|
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||||
additionalSettings['trackOnly'] = true;
|
additionalSettings['trackOnly'] = true;
|
||||||
}
|
}
|
||||||
var trackOnly = additionalSettings['trackOnly'] == true;
|
var trackOnly = additionalSettings['trackOnly'] == true;
|
||||||
String standardUrl = source.standardizeUrl(url);
|
String standardUrl = source.standardizeUrl(url);
|
||||||
APKDetails apk =
|
APKDetails apk = await source.getLatestAPKDetails(
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
standardUrl,
|
||||||
|
additionalSettings,
|
||||||
|
);
|
||||||
|
|
||||||
if (source.runtimeType !=
|
if (source.runtimeType !=
|
||||||
HTML().runtimeType && // Some sources do it separately
|
HTML().runtimeType && // Some sources do it separately
|
||||||
@ -1058,7 +1175,8 @@ class SourceProvider {
|
|||||||
String? extractedVersion = extractVersion(
|
String? extractedVersion = extractVersion(
|
||||||
additionalSettings['versionExtractionRegEx'] as String?,
|
additionalSettings['versionExtractionRegEx'] as String?,
|
||||||
additionalSettings['matchGroupToUse'] as String?,
|
additionalSettings['matchGroupToUse'] as String?,
|
||||||
apk.version);
|
apk.version,
|
||||||
|
);
|
||||||
if (extractedVersion != null) {
|
if (extractedVersion != null) {
|
||||||
apk.version = extractedVersion;
|
apk.version = extractedVersion;
|
||||||
}
|
}
|
||||||
@ -1068,8 +1186,11 @@ class SourceProvider {
|
|||||||
apk.releaseDate != null) {
|
apk.releaseDate != null) {
|
||||||
apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
|
apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
|
||||||
}
|
}
|
||||||
apk.apkUrls = filterApks(apk.apkUrls, additionalSettings['apkFilterRegEx'],
|
apk.apkUrls = filterApks(
|
||||||
additionalSettings['invertAPKFilter']);
|
apk.apkUrls,
|
||||||
|
additionalSettings['apkFilterRegEx'],
|
||||||
|
additionalSettings['invertAPKFilter'],
|
||||||
|
);
|
||||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
@ -1086,8 +1207,10 @@ class SourceProvider {
|
|||||||
(!trackOnly &&
|
(!trackOnly &&
|
||||||
(!source.appIdInferIsOptional ||
|
(!source.appIdInferIsOptional ||
|
||||||
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
||||||
? await source.tryInferringAppId(standardUrl,
|
? await source.tryInferringAppId(
|
||||||
additionalSettings: additionalSettings)
|
standardUrl,
|
||||||
|
additionalSettings: additionalSettings,
|
||||||
|
)
|
||||||
: null) ??
|
: null) ??
|
||||||
generateTempID(standardUrl, additionalSettings),
|
generateTempID(standardUrl, additionalSettings),
|
||||||
standardUrl,
|
standardUrl,
|
||||||
@ -1106,20 +1229,24 @@ class SourceProvider {
|
|||||||
overrideSource: sourceIsOverriden
|
overrideSource: sourceIsOverriden
|
||||||
? source.runtimeType.toString()
|
? source.runtimeType.toString()
|
||||||
: currentApp?.overrideSource,
|
: currentApp?.overrideSource,
|
||||||
allowIdChange: currentApp?.allowIdChange ??
|
allowIdChange:
|
||||||
|
currentApp?.allowIdChange ??
|
||||||
trackOnly ||
|
trackOnly ||
|
||||||
(source.appIdInferIsOptional &&
|
(source.appIdInferIsOptional &&
|
||||||
inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install
|
inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install
|
||||||
otherAssetUrls: apk.allAssetUrls
|
otherAssetUrls: apk.allAssetUrls
|
||||||
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
||||||
.toList());
|
.toList(),
|
||||||
|
);
|
||||||
return source.endOfGetAppChanges(finalApp);
|
return source.endOfGetAppChanges(finalApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
Future<List<dynamic>> getAppsByURLNaive(List<String> urls,
|
Future<List<dynamic>> getAppsByURLNaive(
|
||||||
{List<String> alreadyAddedUrls = const [],
|
List<String> urls, {
|
||||||
AppSource? sourceOverride}) async {
|
List<String> alreadyAddedUrls = const [],
|
||||||
|
AppSource? sourceOverride,
|
||||||
|
}) async {
|
||||||
List<App> apps = [];
|
List<App> apps = [];
|
||||||
Map<String, dynamic> errors = {};
|
Map<String, dynamic> errors = {};
|
||||||
for (var url in urls) {
|
for (var url in urls) {
|
||||||
@ -1128,12 +1255,16 @@ class SourceProvider {
|
|||||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||||
}
|
}
|
||||||
var source = sourceOverride ?? getSource(url);
|
var source = sourceOverride ?? getSource(url);
|
||||||
apps.add(await getApp(
|
apps.add(
|
||||||
|
await getApp(
|
||||||
source,
|
source,
|
||||||
url,
|
url,
|
||||||
sourceIsOverriden: sourceOverride != null,
|
sourceIsOverriden: sourceOverride != null,
|
||||||
getDefaultValuesFromFormItems(
|
getDefaultValuesFromFormItems(
|
||||||
source.combinedAppSpecificSettingFormItems)));
|
source.combinedAppSpecificSettingFormItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.addAll(<String, dynamic>{url: e});
|
errors.addAll(<String, dynamic>{url: e});
|
||||||
}
|
}
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -288,10 +288,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964"
|
sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.9"
|
version: "10.2.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -804,10 +804,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pointer_interceptor_web
|
name: pointer_interceptor_web
|
||||||
sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044"
|
sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.2+1"
|
version: "0.10.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -893,7 +893,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: master
|
ref: master
|
||||||
resolved-ref: "89cdb5434a7ac7510f6bcdb60e1d51a27ee2f40b"
|
resolved-ref: "012e22791138958e089f6c1a8d6c4c6943a9f253"
|
||||||
url: "https://github.com/AlexBacich/shared-storage"
|
url: "https://github.com/AlexBacich/shared-storage"
|
||||||
source: git
|
source: git
|
||||||
version: "0.7.0"
|
version: "0.7.0"
|
||||||
@ -1155,10 +1155,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d"
|
sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1171,10 +1171,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.13.0"
|
version: "5.14.0"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.56+2313
|
version: 1.1.57+2314
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
Reference in New Issue
Block a user