mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-01 05:10:15 +02:00
Merge branch 'main' into re7gog
This commit is contained in:
@@ -271,17 +271,14 @@ class GitHub extends AppSource {
|
||||
}
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)
|
||||
?.map((e) {
|
||||
return (e['name'] != null) &&
|
||||
((e['url'] ?? e['browser_download_url']) != null)
|
||||
? MapEntry(e['name'] as String,
|
||||
(e['url'] ?? e['browser_download_url']) as String)
|
||||
: const MapEntry('', '');
|
||||
})
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList() ??
|
||||
List<MapEntry<String, String>> getReleaseAssetUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)?.map((e) {
|
||||
return (e['name'] != null) &&
|
||||
((e['url'] ?? e['browser_download_url']) != null)
|
||||
? MapEntry(e['name'] as String,
|
||||
(e['url'] ?? e['browser_download_url']) as String)
|
||||
: const MapEntry('', '');
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
DateTime? getPublishDateFromRelease(dynamic rel) =>
|
||||
@@ -383,7 +380,11 @@ class GitHub extends AppSource {
|
||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||
continue;
|
||||
}
|
||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||
var allAssetUrls = getReleaseAssetUrls(releases[i]);
|
||||
List<MapEntry<String, String>> apkUrls = allAssetUrls
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
|
||||
apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||
@@ -391,12 +392,25 @@ class GitHub extends AppSource {
|
||||
}
|
||||
targetRelease = releases[i];
|
||||
targetRelease['apkUrls'] = apkUrls;
|
||||
targetRelease['version'] =
|
||||
targetRelease['tag_name'] ?? targetRelease['name'];
|
||||
if (targetRelease['tarball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
||||
targetRelease['tarball_url']));
|
||||
}
|
||||
if (targetRelease['zipball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.zip',
|
||||
targetRelease['zipball_url']));
|
||||
}
|
||||
targetRelease['allAssetUrls'] = allAssetUrls;
|
||||
break;
|
||||
}
|
||||
if (targetRelease == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? version = targetRelease['tag_name'] ?? targetRelease['name'];
|
||||
String? version = targetRelease['version'];
|
||||
DateTime? releaseDate = getReleaseDateFromRelease(
|
||||
targetRelease, useLatestAssetDateAsReleaseDate);
|
||||
if (version == null) {
|
||||
@@ -408,7 +422,9 @@ class GitHub extends AppSource {
|
||||
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||
getAppNames(standardUrl),
|
||||
releaseDate: releaseDate,
|
||||
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||
changeLog: changeLog.isEmpty ? null : changeLog,
|
||||
allAssetUrls:
|
||||
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
|
||||
} else {
|
||||
if (onHttpErrorCode != null) {
|
||||
onHttpErrorCode(res);
|
||||
|
@@ -180,6 +180,16 @@ class GitLab extends AppSource {
|
||||
throw NoAPKError();
|
||||
}
|
||||
|
||||
return apkDetailsList.first;
|
||||
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
||||
if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$')
|
||||
.hasMatch(apkUrl.value)) {
|
||||
return MapEntry(
|
||||
apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
|
||||
} else {
|
||||
return apkUrl;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
|
@@ -155,7 +155,8 @@ class AddAppPageState extends State<AddAppPage> {
|
||||
// Only download the APK here if you need to for the package ID
|
||||
if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
|
||||
// ignore: use_build_context_synchronously
|
||||
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
||||
var apkUrl =
|
||||
await appsProvider.confirmAppFileUrl(app, context, false);
|
||||
if (apkUrl == null) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
|
@@ -158,6 +158,29 @@ class _AppPageState extends State<AppPage> {
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
if (app?.app.apkUrls.isNotEmpty == true ||
|
||||
app?.app.otherAssetUrls.isNotEmpty == true)
|
||||
GestureDetector(
|
||||
onTap: app?.app == null || updating
|
||||
? null
|
||||
: () async {
|
||||
try {
|
||||
await appsProvider
|
||||
.downloadAppAssets([app!.app.id], context);
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
tr('downloadX', args: [tr('releaseAsset').toLowerCase()]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
decoration:
|
||||
changeLogFn != null ? TextDecoration.underline : null,
|
||||
fontStyle: changeLogFn != null ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
|
@@ -854,69 +854,78 @@ class AppsPageState extends State<AppsPage> {
|
||||
scrollable: true,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
TextButton(
|
||||
onPressed: pinSelectedApps,
|
||||
child: Text(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls += '${a.url}\n';
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('shareSelectedAppURLs'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
? null
|
||||
: () {
|
||||
String urls =
|
||||
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
|
||||
for (var a in selectedApps) {
|
||||
urls +=
|
||||
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
|
||||
'id': a.id,
|
||||
'url': a.url,
|
||||
'author': a.author,
|
||||
'name': a.name,
|
||||
'preferredApkIndex':
|
||||
a.preferredApkIndex,
|
||||
'additionalSettings':
|
||||
jsonEncode(a.additionalSettings)
|
||||
}))}">${a.name}</a></li>\n';
|
||||
}
|
||||
urls +=
|
||||
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
|
||||
Share.share(urls,
|
||||
subject:
|
||||
'Obtainium - ${tr('appsString')}');
|
||||
},
|
||||
child: Text(tr('shareAppConfigLinks'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appsProvider
|
||||
.downloadAppAssets(
|
||||
selectedApps.map((e) => e.id).toList(),
|
||||
globalNavigatorKey.currentContext ??
|
||||
context)
|
||||
.catchError((e) => showError(
|
||||
e,
|
||||
globalNavigatorKey.currentContext ??
|
||||
context));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('downloadX',
|
||||
args: [tr('releaseAsset').toLowerCase()]))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: appsProvider.areDownloadsRunning()
|
||||
? null
|
||||
: showMassMarkDialog,
|
||||
tooltip: tr('markSelectedAppsUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
IconButton(
|
||||
onPressed: pinSelectedApps,
|
||||
tooltip: selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'),
|
||||
icon: Icon(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? Icons.bookmark_outline_rounded
|
||||
: Icons.bookmark_remove_outlined),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls += '${a.url}\n';
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip: tr('shareSelectedAppURLs'),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
? null
|
||||
: () {
|
||||
String urls =
|
||||
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
|
||||
for (var a in selectedApps) {
|
||||
urls +=
|
||||
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
|
||||
'id': a.id,
|
||||
'url': a.url,
|
||||
'author': a.author,
|
||||
'name': a.name,
|
||||
'preferredApkIndex':
|
||||
a.preferredApkIndex,
|
||||
'additionalSettings':
|
||||
jsonEncode(a.additionalSettings)
|
||||
}))}">${a.name}</a></li>\n';
|
||||
}
|
||||
urls +=
|
||||
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
},
|
||||
tooltip: tr('shareAppConfigLinks'),
|
||||
icon: const Icon(Icons.ios_share),
|
||||
),
|
||||
child: Text(tr('markSelectedAppsUpdated'))),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
@@ -360,7 +360,7 @@ class AppsProvider with ChangeNotifier {
|
||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||
isForeground = event == FGBGType.foreground;
|
||||
if (isForeground) await loadApps();
|
||||
if (isForeground) loadApps();
|
||||
});
|
||||
() async {
|
||||
await settingsProvider.initializeSettings();
|
||||
@@ -698,23 +698,28 @@ class AppsProvider with ChangeNotifier {
|
||||
await intent.launch();
|
||||
}
|
||||
|
||||
Future<MapEntry<String, String>?> confirmApkUrl(
|
||||
App app, BuildContext? context) async {
|
||||
Future<MapEntry<String, String>?> confirmAppFileUrl(
|
||||
App app, BuildContext? context, bool pickAnyAsset) async {
|
||||
var urlsToSelectFrom = app.apkUrls;
|
||||
if (pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls];
|
||||
}
|
||||
// If the App has more than one APK, the user should pick one (if context provided)
|
||||
MapEntry<String, String>? apkUrl =
|
||||
app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
MapEntry<String, String>? appFileUrl = urlsToSelectFrom[
|
||||
app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
// get device supported architecture
|
||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||
|
||||
if (app.apkUrls.length > 1 && context != null) {
|
||||
if (urlsToSelectFrom.length > 1 && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await showDialog(
|
||||
appFileUrl = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKPicker(
|
||||
return AppFilePicker(
|
||||
app: app,
|
||||
initVal: apkUrl,
|
||||
initVal: appFileUrl,
|
||||
archs: archs,
|
||||
pickAnyAsset: pickAnyAsset,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -724,8 +729,8 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||
if (apkUrl != null &&
|
||||
getHost(apkUrl.value) != getHost(app.url) &&
|
||||
if (appFileUrl != null &&
|
||||
getHost(appFileUrl.value) != getHost(app.url) &&
|
||||
context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||
@@ -734,13 +739,13 @@ class AppsProvider with ChangeNotifier {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKOriginWarningDialog(
|
||||
sourceUrl: app.url, apkUrl: apkUrl!.value);
|
||||
sourceUrl: app.url, apkUrl: appFileUrl!.value);
|
||||
}) !=
|
||||
true) {
|
||||
apkUrl = null;
|
||||
appFileUrl = null;
|
||||
}
|
||||
}
|
||||
return apkUrl;
|
||||
return appFileUrl;
|
||||
}
|
||||
|
||||
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||
@@ -767,7 +772,7 @@ class AppsProvider with ChangeNotifier {
|
||||
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
|
||||
if (!trackOnly) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
||||
apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false);
|
||||
}
|
||||
if (apkUrl != null) {
|
||||
int urlInd = apps[id]!
|
||||
@@ -898,6 +903,85 @@ class AppsProvider with ChangeNotifier {
|
||||
return installedIds;
|
||||
}
|
||||
|
||||
Future<List<String>> downloadAppAssets(
|
||||
List<String> appIds, BuildContext context,
|
||||
{bool forceParallelDownloads = false}) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
List<MapEntry<MapEntry<String, String>, App>> filesToDownload = [];
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw ObtainiumError(tr('appNotFound'));
|
||||
}
|
||||
MapEntry<String, String>? fileUrl;
|
||||
if (apps[id]!.app.apkUrls.isNotEmpty ||
|
||||
apps[id]!.app.otherAssetUrls.isNotEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true);
|
||||
}
|
||||
if (fileUrl != null) {
|
||||
filesToDownload.add(MapEntry(fileUrl, apps[id]!.app));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to download+install Apps
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
List<String> downloadedIds = [];
|
||||
|
||||
Future<void> downloadFn(MapEntry<String, String> fileUrl, App app) async {
|
||||
try {
|
||||
var exportDir = await settingsProvider.getExportDir();
|
||||
String downloadPath = '/storage/emulated/0/Download';
|
||||
bool downloadsAccessible = false;
|
||||
try {
|
||||
downloadsAccessible = Directory(downloadPath).existsSync();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if (!downloadsAccessible && exportDir != null) {
|
||||
downloadPath = exportDir.path;
|
||||
}
|
||||
await downloadFile(
|
||||
fileUrl.value,
|
||||
fileUrl.key
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList()
|
||||
.sublist(1)
|
||||
.reversed
|
||||
.join('.'), (double? progress) {
|
||||
notificationsProvider
|
||||
.notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0));
|
||||
}, downloadPath,
|
||||
headers: await SourceProvider()
|
||||
.getSource(app.url, overrideSource: app.overrideSource)
|
||||
.getRequestHeaders(app.additionalSettings,
|
||||
forAPKDownload:
|
||||
fileUrl.key.endsWith('.apk') ? true : false),
|
||||
useExisting: false);
|
||||
notificationsProvider
|
||||
.notify(DownloadedNotification(fileUrl.key, fileUrl.value));
|
||||
} catch (e) {
|
||||
errors.add(fileUrl.key, e);
|
||||
} finally {
|
||||
notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
|
||||
for (var urlWithApp in filesToDownload) {
|
||||
await downloadFn(urlWithApp.key, urlWithApp.value);
|
||||
}
|
||||
} else {
|
||||
await Future.wait(filesToDownload
|
||||
.map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value)));
|
||||
}
|
||||
if (errors.idsByErrorString.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
return downloadedIds;
|
||||
}
|
||||
|
||||
Future<Directory> getAppsDir() async {
|
||||
Directory appsDir =
|
||||
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
|
||||
@@ -1469,38 +1553,49 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class APKPicker extends StatefulWidget {
|
||||
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
||||
class AppFilePicker extends StatefulWidget {
|
||||
const AppFilePicker(
|
||||
{super.key,
|
||||
required this.app,
|
||||
this.initVal,
|
||||
this.archs,
|
||||
this.pickAnyAsset = false});
|
||||
|
||||
final App app;
|
||||
final MapEntry<String, String>? initVal;
|
||||
final List<String>? archs;
|
||||
final bool pickAnyAsset;
|
||||
|
||||
@override
|
||||
State<APKPicker> createState() => _APKPickerState();
|
||||
State<AppFilePicker> createState() => _AppFilePickerState();
|
||||
}
|
||||
|
||||
class _APKPickerState extends State<APKPicker> {
|
||||
MapEntry<String, String>? apkUrl;
|
||||
class _AppFilePickerState extends State<AppFilePicker> {
|
||||
MapEntry<String, String>? fileUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
apkUrl ??= widget.initVal;
|
||||
fileUrl ??= widget.initVal;
|
||||
var urlsToSelectFrom = widget.app.apkUrls;
|
||||
if (widget.pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...widget.app.otherAssetUrls];
|
||||
}
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(tr('pickAnAPK')),
|
||||
title: Text(widget.pickAnyAsset
|
||||
? tr('selectX', args: [tr('releaseAsset').toLowerCase()])
|
||||
: tr('pickAnAPK')),
|
||||
content: Column(children: [
|
||||
Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])),
|
||||
const SizedBox(height: 16),
|
||||
...widget.app.apkUrls.map(
|
||||
...urlsToSelectFrom.map(
|
||||
(u) => RadioListTile<String>(
|
||||
title: Text(u.key),
|
||||
value: u.value,
|
||||
groupValue: apkUrl!.value,
|
||||
groupValue: fileUrl!.value,
|
||||
onChanged: (String? val) {
|
||||
setState(() {
|
||||
apkUrl =
|
||||
widget.app.apkUrls.where((e) => e.value == val).first;
|
||||
fileUrl = urlsToSelectFrom.where((e) => e.value == val).first;
|
||||
});
|
||||
}),
|
||||
),
|
||||
@@ -1527,7 +1622,7 @@ class _APKPickerState extends State<APKPicker> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(apkUrl);
|
||||
Navigator.of(context).pop(fileUrl);
|
||||
},
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
|
@@ -120,6 +120,18 @@ class DownloadNotification extends ObtainiumNotification {
|
||||
progPercent: progPercent);
|
||||
}
|
||||
|
||||
class DownloadedNotification extends ObtainiumNotification {
|
||||
DownloadedNotification(String fileName, String downloadUrl)
|
||||
: super(
|
||||
downloadUrl.hashCode,
|
||||
tr('downloadedX', args: [fileName]),
|
||||
'',
|
||||
'FILE_DOWNLOADED',
|
||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||
tr('downloadedX', args: [tr('app')]),
|
||||
Importance.defaultImportance);
|
||||
}
|
||||
|
||||
final completeInstallationNotification = ObtainiumNotification(
|
||||
1,
|
||||
tr('completeAppInstallation'),
|
||||
|
@@ -47,9 +47,10 @@ class APKDetails {
|
||||
late AppNames names;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
late List<MapEntry<String, String>> allAssetUrls;
|
||||
|
||||
APKDetails(this.version, this.apkUrls, this.names,
|
||||
{this.releaseDate, this.changeLog});
|
||||
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
|
||||
}
|
||||
|
||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||
@@ -223,6 +224,7 @@ class App {
|
||||
String? installedVersion;
|
||||
late String latestVersion;
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
List<MapEntry<String, String>> otherAssetUrls = [];
|
||||
late int preferredApkIndex;
|
||||
late Map<String, dynamic> additionalSettings;
|
||||
late DateTime? lastUpdateCheck;
|
||||
@@ -248,7 +250,8 @@ class App {
|
||||
this.releaseDate,
|
||||
this.changeLog,
|
||||
this.overrideSource,
|
||||
this.allowIdChange = false});
|
||||
this.allowIdChange = false,
|
||||
this.otherAssetUrls = const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -280,41 +283,44 @@ class App {
|
||||
changeLog: changeLog,
|
||||
releaseDate: releaseDate,
|
||||
overrideSource: overrideSource,
|
||||
allowIdChange: allowIdChange);
|
||||
allowIdChange: allowIdChange,
|
||||
otherAssetUrls: otherAssetUrls);
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) {
|
||||
json = appJSONCompatibilityModifiers(json);
|
||||
return App(
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(jsonDecode(
|
||||
(json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog:
|
||||
json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false);
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(
|
||||
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog: json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false,
|
||||
otherAssetUrls: assumed2DlistToStringMapList(
|
||||
jsonDecode((json['otherAssetUrls'] ?? '[]'))),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -325,6 +331,7 @@ class App {
|
||||
'installedVersion': installedVersion,
|
||||
'latestVersion': latestVersion,
|
||||
'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
|
||||
'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)),
|
||||
'preferredApkIndex': preferredApkIndex,
|
||||
'additionalSettings': jsonEncode(additionalSettings),
|
||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||
@@ -892,8 +899,10 @@ class SourceProvider {
|
||||
allowIdChange: currentApp?.allowIdChange ??
|
||||
trackOnly ||
|
||||
(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
|
||||
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
||||
.toList());
|
||||
return source.endOfGetAppChanges(finalApp);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user