Adds Track-Only App Support (Addresses #119 and Sets Groundwork for #44) (#123)

- All Sources now have a "Track-Only" option that will prevent Obtainium from looking for APKs (though the App must still have a release of some kind so that a version string can be grabbed).
    - These Apps cannot be installed through Obtainium, but update notifications will still be sent.
    - The user needs to manually mark them as updated when appropriate.
    - This addresses issue #119.
    - It also partially addresses #44 by allowing some sources to be configured as "Track-Only"-only. The first such source (APKMirror) will be added later.
- Includes various UI changes to accommodate the above change.
- Also makes App loading a bit more responsive (sending Obtainium to the background then returning will now cause App re-load to pick up changes in App versioning that may have been made in the meantime, for instance through update checking).
This commit is contained in:
Imran Remtulla
2022-11-24 21:12:46 -05:00
committed by GitHub
parent 868ba84c9a
commit b04d2fad5c
16 changed files with 341 additions and 145 deletions

View File

@@ -266,6 +266,7 @@ class AppsProvider with ChangeNotifier {
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context) async {
List<String> appsToInstall = [];
List<String> trackOnlyAppsToUpdate = [];
// For all specified Apps, filter out those for which:
// 1. A URL cannot be picked
// 2. That cannot be installed silently (IF no buildContext was given for interactive install)
@@ -273,7 +274,10 @@ class AppsProvider with ChangeNotifier {
if (apps[id] == null) {
throw ObtainiumError('App not found');
}
String? apkUrl = await confirmApkUrl(apps[id]!.app, context);
String? apkUrl;
if (!apps[id]!.app.trackOnly) {
apkUrl = await confirmApkUrl(apps[id]!.app, context);
}
if (apkUrl != null) {
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
if (urlInd != apps[id]!.app.preferredApkIndex) {
@@ -284,7 +288,16 @@ class AppsProvider with ChangeNotifier {
appsToInstall.add(id);
}
}
if (apps[id]!.app.trackOnly) {
trackOnlyAppsToUpdate.add(id);
}
}
// Mark all specified track-only apps as latest
saveApps(trackOnlyAppsToUpdate.map((e) {
var a = apps[e]!.app;
a.installedVersion = a.latestVersion;
return a;
}).toList());
// Download APKs for all Apps to be installed
MultiAppMultiError errors = MultiAppMultiError();
List<DownloadedApk?> downloadedFiles =
@@ -391,7 +404,9 @@ class AppsProvider with ChangeNotifier {
return null; // Can't correct in the background isolate
}
var modded = false;
if (installedInfo == null && app.installedVersion != null) {
if (installedInfo == null &&
app.installedVersion != null &&
!app.trackOnly) {
app.installedVersion = null;
modded = true;
}
@@ -445,8 +460,7 @@ class AppsProvider with ChangeNotifier {
var info = await getInstalledInfo(newApps[i].id);
try {
sp.getSource(newApps[i].url);
apps.putIfAbsent(
newApps[i].id, () => AppInMemory(newApps[i], null, info));
apps[newApps[i].id] = AppInMemory(newApps[i], null, info);
} catch (e) {
errors.add([newApps[i].id, newApps[i].name, e.toString()]);
}
@@ -512,8 +526,9 @@ class AppsProvider with ChangeNotifier {
currentApp.additionalData,
name: currentApp.name,
id: currentApp.id,
pinned: currentApp.pinned);
newApp.installedVersion = currentApp.installedVersion;
pinned: currentApp.pinned,
trackOnly: currentApp.trackOnly,
installedVersion: currentApp.installedVersion);
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
newApp.preferredApkIndex = currentApp.preferredApkIndex;
}

View File

@@ -42,6 +42,7 @@ class App {
late List<String> additionalData;
late DateTime? lastUpdateCheck;
bool pinned = false;
bool trackOnly = false;
App(
this.id,
this.url,
@@ -53,7 +54,8 @@ class App {
this.preferredApkIndex,
this.additionalData,
this.lastUpdateCheck,
this.pinned);
this.pinned,
this.trackOnly);
@override
String toString() {
@@ -74,12 +76,15 @@ class App {
: List<String>.from(jsonDecode(json['apkUrls'])),
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
json['additionalData'] == null
? SourceProvider().getSource(json['url']).additionalDataDefaults
? SourceProvider()
.getSource(json['url'])
.additionalSourceAppSpecificDefaults
: List<String>.from(jsonDecode(json['additionalData'])),
json['lastUpdateCheck'] == null
? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
json['pinned'] ?? false);
json['pinned'] ?? false,
json['trackOnly'] ?? false);
Map<String, dynamic> toJson() => {
'id': id,
@@ -92,7 +97,8 @@ class App {
'preferredApkIndex': preferredApkIndex,
'additionalData': jsonEncode(additionalData),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned
'pinned': pinned,
'trackOnly': trackOnly
};
}
@@ -135,6 +141,7 @@ List<String> getLinksFromParsedHTML(
class AppSource {
late String host;
bool enforceTrackOnly = false;
String standardizeURL(String url) {
throw NotImplementedError();
}
@@ -148,9 +155,22 @@ class AppSource {
throw NotImplementedError();
}
List<List<GeneratedFormItem>> additionalDataFormItems = [];
List<String> additionalDataDefaults = [];
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
// Different Sources may need different kinds of additional data for Apps
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = [];
List<String> additionalSourceAppSpecificDefaults = [];
// Some additional data may be needed for Apps regardless of Source
final List<GeneratedFormItem> additionalAppSpecificSourceAgnosticFormItems = [
GeneratedFormItem(
label: 'Track-Only',
type: FormItemType.bool,
key: 'trackOnlyFormItemKey')
];
final List<String> additionalAppSpecificSourceAgnosticDefaults = [''];
// Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
String? changeLogPageFromStandardUrl(String standardUrl) {
throw NotImplementedError();
}
@@ -211,7 +231,7 @@ class SourceProvider {
}
bool ifSourceAppsRequireAdditionalData(AppSource source) {
for (var row in source.additionalDataFormItems) {
for (var row in source.additionalSourceAppSpecificFormItems) {
for (var element in row) {
if (element.required) {
return true;
@@ -238,11 +258,19 @@ class SourceProvider {
}
Future<App> getApp(AppSource source, String url, List<String> additionalData,
{String name = '', String? id, bool pinned = false}) async {
{String name = '',
String? id,
bool pinned = false,
bool trackOnly = false,
String? installedVersion}) async {
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
AppNames names = source.getAppNames(standardUrl);
APKDetails apk =
await source.getLatestAPKDetails(standardUrl, additionalData);
if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError();
}
String apkVersion = apk.version.replaceAll('/', '-');
return App(
id ??
source.tryInferringAppId(standardUrl) ??
@@ -252,24 +280,26 @@ class SourceProvider {
name.trim().isNotEmpty
? name
: names.name[0].toUpperCase() + names.name.substring(1),
null,
apk.version.replaceAll('/', '-'),
installedVersion,
apkVersion,
apk.apkUrls,
apk.apkUrls.length - 1,
additionalData,
DateTime.now(),
pinned);
pinned,
trackOnly);
}
// Returns errors in [results, errors] instead of throwing them
Future<List<dynamic>> getApps(List<String> urls,
Future<List<dynamic>> getAppsByURLNaive(List<String> urls,
{List<String> ignoreUrls = const []}) async {
List<App> apps = [];
Map<String, dynamic> errors = {};
for (var url in urls.where((element) => !ignoreUrls.contains(element))) {
try {
var source = getSource(url);
apps.add(await getApp(source, url, source.additionalDataDefaults));
apps.add(await getApp(
source, url, source.additionalSourceAppSpecificDefaults));
} catch (e) {
errors.addAll(<String, dynamic>{url: e});
}