mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-16 06:36:44 +02:00
- 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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
|
@ -48,9 +48,6 @@ class FDroid extends AppSource {
|
|||||||
.where((element) => element['versionName'] == latestVersion)
|
.where((element) => element['versionName'] == latestVersion)
|
||||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.toList();
|
.toList();
|
||||||
if (apkUrls.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
return APKDetails(latestVersion, apkUrls);
|
return APKDetails(latestVersion, apkUrls);
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
|
@ -11,9 +11,9 @@ class GitHub extends AppSource {
|
|||||||
GitHub() {
|
GitHub() {
|
||||||
host = 'github.com';
|
host = 'github.com';
|
||||||
|
|
||||||
additionalDataDefaults = ['true', 'true', ''];
|
additionalSourceAppSpecificDefaults = ['true', 'true', ''];
|
||||||
|
|
||||||
moreSourceSettingsFormItems = [
|
additionalSourceSpecificSettingFormItems = [
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
||||||
id: 'github-creds',
|
id: 'github-creds',
|
||||||
@ -51,7 +51,7 @@ class GitHub extends AppSource {
|
|||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
additionalDataFormItems = [
|
additionalSourceAppSpecificFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)
|
GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)
|
||||||
],
|
],
|
||||||
@ -96,8 +96,8 @@ class GitHub extends AppSource {
|
|||||||
Future<String> getCredentialPrefixIfAny() async {
|
Future<String> getCredentialPrefixIfAny() async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
String? creds =
|
String? creds = settingsProvider
|
||||||
settingsProvider.getSettingString(moreSourceSettingsFormItems[0].id);
|
.getSettingString(additionalSourceSpecificSettingFormItems[0].id);
|
||||||
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,14 +155,11 @@ class GitHub extends AppSource {
|
|||||||
if (targetRelease == null) {
|
if (targetRelease == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
if ((targetRelease['apkUrls'] as List<String>).isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
String? version = targetRelease['tag_name'];
|
String? version = targetRelease['tag_name'];
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, targetRelease['apkUrls']);
|
return APKDetails(version, targetRelease['apkUrls'] as List<String>);
|
||||||
} else {
|
} else {
|
||||||
rateLimitErrorCheck(res);
|
rateLimitErrorCheck(res);
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
|
@ -33,7 +33,7 @@ class GitLab extends AppSource {
|
|||||||
var entry = parsedHtml.querySelector('entry');
|
var entry = parsedHtml.querySelector('entry');
|
||||||
var entryContent =
|
var entryContent =
|
||||||
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
|
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
|
||||||
var apkUrlList = [
|
var apkUrls = [
|
||||||
...getLinksFromParsedHTML(
|
...getLinksFromParsedHTML(
|
||||||
entryContent,
|
entryContent,
|
||||||
RegExp(
|
RegExp(
|
||||||
@ -48,9 +48,6 @@ class GitLab extends AppSource {
|
|||||||
.where((element) => Uri.parse(element).host != '')
|
.where((element) => Uri.parse(element).host != '')
|
||||||
.toList()
|
.toList()
|
||||||
];
|
];
|
||||||
if (apkUrlList.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
|
|
||||||
var entryId = entry?.querySelector('id')?.innerHtml;
|
var entryId = entry?.querySelector('id')?.innerHtml;
|
||||||
var version =
|
var version =
|
||||||
@ -58,7 +55,7 @@ class GitLab extends AppSource {
|
|||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, apkUrlList);
|
return APKDetails(version, apkUrls);
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
|
@ -24,14 +24,12 @@ class Signal extends AppSource {
|
|||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
String? apkUrl = json['url'];
|
String? apkUrl = json['url'];
|
||||||
if (apkUrl == null) {
|
List<String> apkUrls = apkUrl == null ? [] : [apkUrl];
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
String? version = json['versionName'];
|
String? version = json['versionName'];
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, [apkUrl]);
|
return APKDetails(version, apkUrls);
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
|
@ -49,9 +49,6 @@ class SourceForge extends AppSource {
|
|||||||
apkUrlListAllReleases // This can be used skipped for fallback support later
|
apkUrlListAllReleases // This can be used skipped for fallback support later
|
||||||
.where((element) => getVersion(element) == version)
|
.where((element) => getVersion(element) == version)
|
||||||
.toList();
|
.toList();
|
||||||
if (apkUrlList.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
return APKDetails(version, apkUrlList);
|
return APKDetails(version, apkUrlList);
|
||||||
} else {
|
} else {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
|
@ -6,6 +6,7 @@ typedef OnValueChanges = void Function(
|
|||||||
List<String> values, bool valid, bool isBuilding);
|
List<String> values, bool valid, bool isBuilding);
|
||||||
|
|
||||||
class GeneratedFormItem {
|
class GeneratedFormItem {
|
||||||
|
late String key;
|
||||||
late String label;
|
late String label;
|
||||||
late FormItemType type;
|
late FormItemType type;
|
||||||
late bool required;
|
late bool required;
|
||||||
@ -25,7 +26,8 @@ class GeneratedFormItem {
|
|||||||
this.id = 'input',
|
this.id = 'input',
|
||||||
this.belowWidgets = const [],
|
this.belowWidgets = const [],
|
||||||
this.hint,
|
this.hint,
|
||||||
this.opts});
|
this.opts,
|
||||||
|
this.key = 'default'});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedForm extends StatefulWidget {
|
class GeneratedForm extends StatefulWidget {
|
||||||
@ -209,3 +211,18 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? findGeneratedFormValueByKey(
|
||||||
|
List<GeneratedFormItem> items, List<String> values, String key) {
|
||||||
|
var foundIndex = -1;
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].key == key) {
|
||||||
|
foundIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundIndex >= 0 && foundIndex < values.length) {
|
||||||
|
return values[foundIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@ -29,7 +29,7 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
values = widget.defaultValues;
|
values = widget.defaultValues;
|
||||||
valid = widget.initValid;
|
valid = widget.initValid || widget.items.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -154,6 +154,7 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
0,
|
0,
|
||||||
['true'],
|
['true'],
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
false)
|
false)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
@ -22,8 +23,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
|
|
||||||
String userInput = '';
|
String userInput = '';
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
List<String> additionalData = [];
|
List<String> sourceSpecificAdditionalData = [];
|
||||||
bool validAdditionalData = true;
|
bool sourceSpecificDataIsValid = true;
|
||||||
|
List<String> otherAdditionalData = [];
|
||||||
|
bool otherAdditionalDataIsValid = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -67,23 +70,34 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
setState(() {
|
fn() {
|
||||||
userInput = values[0];
|
userInput = values[0];
|
||||||
var source = valid
|
var source = valid
|
||||||
? sourceProvider.getSource(userInput)
|
? sourceProvider.getSource(userInput)
|
||||||
: null;
|
: null;
|
||||||
if (pickedSource != source) {
|
if (pickedSource != source) {
|
||||||
pickedSource = source;
|
pickedSource = source;
|
||||||
additionalData = source != null
|
sourceSpecificAdditionalData = source !=
|
||||||
? source.additionalDataDefaults
|
null
|
||||||
|
? source
|
||||||
|
.additionalSourceAppSpecificDefaults
|
||||||
: [];
|
: [];
|
||||||
validAdditionalData = source != null
|
sourceSpecificDataIsValid = source !=
|
||||||
|
null
|
||||||
? sourceProvider
|
? sourceProvider
|
||||||
.ifSourceAppsRequireAdditionalData(
|
.ifSourceAppsRequireAdditionalData(
|
||||||
source)
|
source)
|
||||||
: true;
|
: true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBuilding) {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
fn();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
defaultValues: const [])),
|
defaultValues: const [])),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -94,9 +108,14 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: gettingAppInfo ||
|
onPressed: gettingAppInfo ||
|
||||||
pickedSource == null ||
|
pickedSource == null ||
|
||||||
(pickedSource!.additionalDataFormItems
|
(pickedSource!
|
||||||
|
.additionalSourceAppSpecificFormItems
|
||||||
.isNotEmpty &&
|
.isNotEmpty &&
|
||||||
!validAdditionalData)
|
!sourceSpecificDataIsValid) ||
|
||||||
|
(pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticDefaults
|
||||||
|
.isNotEmpty &&
|
||||||
|
!otherAdditionalDataIsValid)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -107,20 +126,52 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
var settingsProvider =
|
var settingsProvider =
|
||||||
context.read<SettingsProvider>();
|
context.read<SettingsProvider>();
|
||||||
() async {
|
() async {
|
||||||
|
var userPickedTrackOnly =
|
||||||
|
findGeneratedFormValueByKey(
|
||||||
|
pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticFormItems,
|
||||||
|
otherAdditionalData,
|
||||||
|
'trackOnlyFormItemKey') ==
|
||||||
|
'true';
|
||||||
|
var cont = true;
|
||||||
|
if ((userPickedTrackOnly ||
|
||||||
|
pickedSource!
|
||||||
|
.enforceTrackOnly) &&
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title:
|
||||||
|
'App is Track-Only',
|
||||||
|
items: const [],
|
||||||
|
defaultValues: const [],
|
||||||
|
message:
|
||||||
|
'${pickedSource!.enforceTrackOnly ? 'Apps from this source are \'Track-Only\'.' : 'You have selected the \'Track-Only\' option.'}\n\nThe App will be tracked for updates, but Obtainium will not be able to download or install it.',
|
||||||
|
);
|
||||||
|
}) ==
|
||||||
|
null) {
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
if (cont) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
App app =
|
App app = await sourceProvider.getApp(
|
||||||
await sourceProvider.getApp(
|
|
||||||
pickedSource!,
|
pickedSource!,
|
||||||
userInput,
|
userInput,
|
||||||
additionalData);
|
sourceSpecificAdditionalData,
|
||||||
|
trackOnly: pickedSource!
|
||||||
|
.enforceTrackOnly ||
|
||||||
|
userPickedTrackOnly);
|
||||||
await settingsProvider
|
await settingsProvider
|
||||||
.getInstallPermission();
|
.getInstallPermission();
|
||||||
// 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 (sourceProvider
|
if (sourceProvider
|
||||||
.isTempId(app.id)) {
|
.isTempId(app.id) &&
|
||||||
|
!app.trackOnly) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var apkUrl = await appsProvider
|
var apkUrl = await appsProvider
|
||||||
.confirmApkUrl(app, context);
|
.confirmApkUrl(
|
||||||
|
app, context);
|
||||||
if (apkUrl == null) {
|
if (apkUrl == null) {
|
||||||
throw ObtainiumError(
|
throw ObtainiumError(
|
||||||
'Cancelled');
|
'Cancelled');
|
||||||
@ -137,17 +188,25 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
throw ObtainiumError(
|
throw ObtainiumError(
|
||||||
'App already added');
|
'App already added');
|
||||||
}
|
}
|
||||||
await appsProvider.saveApps([app]);
|
if (app.trackOnly) {
|
||||||
|
app.installedVersion =
|
||||||
|
app.latestVersion;
|
||||||
|
}
|
||||||
|
await appsProvider
|
||||||
|
.saveApps([app]);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
.then((app) {
|
.then((app) {
|
||||||
|
if (app != null) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
AppPage(
|
AppPage(
|
||||||
appId: app.id)));
|
appId: app.id)));
|
||||||
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
}).whenComplete(() {
|
||||||
@ -160,7 +219,11 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null &&
|
if (pickedSource != null &&
|
||||||
pickedSource!.additionalDataDefaults.isNotEmpty)
|
(pickedSource!.additionalSourceAppSpecificDefaults
|
||||||
|
.isNotEmpty ||
|
||||||
|
pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticDefaults
|
||||||
|
.isNotEmpty))
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -176,19 +239,54 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (pickedSource!
|
if (pickedSource!
|
||||||
.additionalDataFormItems.isNotEmpty)
|
.additionalSourceAppSpecificFormItems
|
||||||
|
.isNotEmpty)
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
items: pickedSource!.additionalDataFormItems,
|
items: pickedSource!
|
||||||
|
.additionalSourceAppSpecificFormItems,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
if (isBuilding) {
|
||||||
|
sourceSpecificAdditionalData = values;
|
||||||
|
sourceSpecificDataIsValid = valid;
|
||||||
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
additionalData = values;
|
sourceSpecificAdditionalData = values;
|
||||||
validAdditionalData = valid;
|
sourceSpecificDataIsValid = valid;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
defaultValues:
|
defaultValues: pickedSource!
|
||||||
pickedSource!.additionalDataDefaults),
|
.additionalSourceAppSpecificDefaults),
|
||||||
if (pickedSource!
|
if (pickedSource!
|
||||||
.additionalDataFormItems.isNotEmpty)
|
.additionalSourceAppSpecificFormItems
|
||||||
|
.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
if (pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticFormItems
|
||||||
|
.isNotEmpty)
|
||||||
|
GeneratedForm(
|
||||||
|
items: pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticFormItems
|
||||||
|
.map((e) => [e])
|
||||||
|
.toList(),
|
||||||
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
if (isBuilding) {
|
||||||
|
otherAdditionalData = values;
|
||||||
|
otherAdditionalDataIsValid = valid;
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
otherAdditionalData = values;
|
||||||
|
otherAdditionalDataIsValid = valid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValues: pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticDefaults),
|
||||||
|
if (pickedSource!
|
||||||
|
.additionalAppSpecificSourceAgnosticDefaults
|
||||||
|
.isNotEmpty)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
|
@ -106,7 +106,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Installed Version: ${app?.app.installedVersion ?? 'None'}',
|
'Installed Version: ${app?.app.installedVersion ?? 'None'}${app?.app.trackOnly == true ? ' (Estimate)\n\nApp is Track-Only' : ''}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
@ -140,6 +140,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
if (app?.app.installedVersion != null &&
|
if (app?.app.installedVersion != null &&
|
||||||
|
app?.app.trackOnly == false &&
|
||||||
app?.app.installedVersion != app?.app.latestVersion)
|
app?.app.installedVersion != app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
@ -183,7 +184,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
tooltip: 'Mark as Updated',
|
tooltip: 'Mark as Updated',
|
||||||
icon: const Icon(Icons.done)),
|
icon: const Icon(Icons.done)),
|
||||||
if (source != null &&
|
if (source != null &&
|
||||||
source.additionalDataFormItems.isNotEmpty)
|
source.additionalSourceAppSpecificFormItems
|
||||||
|
.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
@ -194,11 +196,11 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: 'Additional Options',
|
title: 'Additional Options',
|
||||||
items: source
|
items: source
|
||||||
.additionalDataFormItems,
|
.additionalSourceAppSpecificFormItems,
|
||||||
defaultValues: app != null
|
defaultValues: app != null
|
||||||
? app.app.additionalData
|
? app.app.additionalData
|
||||||
: source
|
: source
|
||||||
.additionalDataDefaults);
|
.additionalSourceAppSpecificDefaults);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (app != null && values != null) {
|
if (app != null && values != null) {
|
||||||
var changedApp = app.app;
|
var changedApp = app.app;
|
||||||
@ -234,8 +236,12 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(app?.app.installedVersion == null
|
child: Text(app?.app.installedVersion == null
|
||||||
|
? app?.app.trackOnly == false
|
||||||
? 'Install'
|
? 'Install'
|
||||||
: 'Update'))),
|
: 'Mark Installed'
|
||||||
|
: app?.app.trackOnly == false
|
||||||
|
? 'Update'
|
||||||
|
: 'Mark Updated'))),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
|
@ -135,6 +135,22 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: selectedApps.map((e) => e.id).contains(element))
|
: selectedApps.map((e) => e.id).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
List<String> trackOnlyUpdateIdsAllOrSelected = [];
|
||||||
|
existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
|
||||||
|
if (appsProvider.apps[id]!.app.trackOnly) {
|
||||||
|
trackOnlyUpdateIdsAllOrSelected.add(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
|
||||||
|
if (appsProvider.apps[id]!.app.trackOnly) {
|
||||||
|
trackOnlyUpdateIdsAllOrSelected.add(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
if (settingsProvider.pinUpdates) {
|
if (settingsProvider.pinUpdates) {
|
||||||
var temp = [];
|
var temp = [];
|
||||||
sortedApps = sortedApps.where((sa) {
|
sortedApps = sortedApps.where((sa) {
|
||||||
@ -245,7 +261,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(appsProvider.areDownloadsRunning()
|
Text(appsProvider.areDownloadsRunning()
|
||||||
? 'Please Wait...'
|
? 'Please Wait...'
|
||||||
: 'Update Available'),
|
: 'Update Available${sortedApps[index].app.trackOnly ? ' (Est.)' : ''}'),
|
||||||
SourceProvider()
|
SourceProvider()
|
||||||
.getSource(sortedApps[index].app.url)
|
.getSource(sortedApps[index].app.url)
|
||||||
.changeLogPageFromStandardUrl(
|
.changeLogPageFromStandardUrl(
|
||||||
@ -276,8 +292,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
child: Text(
|
child: Text(
|
||||||
sortedApps[index].app.installedVersion ??
|
'${sortedApps[index].app.installedVersion ?? 'Not Installed'} ${sortedApps[index].app.trackOnly == true ? '(Estimate)' : ''}',
|
||||||
'Not Installed',
|
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
)))),
|
)))),
|
||||||
@ -349,50 +364,70 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: appsProvider.areDownloadsRunning() ||
|
onPressed: appsProvider.areDownloadsRunning() ||
|
||||||
(existingUpdateIdsAllOrSelected.isEmpty &&
|
(existingUpdateIdsAllOrSelected.isEmpty &&
|
||||||
newInstallIdsAllOrSelected.isEmpty)
|
newInstallIdsAllOrSelected.isEmpty &&
|
||||||
|
trackOnlyUpdateIdsAllOrSelected.isEmpty)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
List<List<GeneratedFormItem>> formInputs = [];
|
List<GeneratedFormItem> formInputs = [];
|
||||||
if (existingUpdateIdsAllOrSelected.isNotEmpty &&
|
List<String> defaultValues = [];
|
||||||
newInstallIdsAllOrSelected.isNotEmpty) {
|
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
|
||||||
formInputs.add([
|
formInputs.add(GeneratedFormItem(
|
||||||
GeneratedFormItem(
|
|
||||||
label:
|
label:
|
||||||
'Update ${existingUpdateIdsAllOrSelected.length} App${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'}',
|
'Update ${existingUpdateIdsAllOrSelected.length} App${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||||
type: FormItemType.bool)
|
type: FormItemType.bool,
|
||||||
]);
|
key: 'updates'));
|
||||||
formInputs.add([
|
defaultValues.add('true');
|
||||||
GeneratedFormItem(
|
}
|
||||||
|
if (newInstallIdsAllOrSelected.isNotEmpty) {
|
||||||
|
formInputs.add(GeneratedFormItem(
|
||||||
label:
|
label:
|
||||||
'Install ${newInstallIdsAllOrSelected.length} new App${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}',
|
'Install ${newInstallIdsAllOrSelected.length} new App${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||||
type: FormItemType.bool)
|
type: FormItemType.bool,
|
||||||
]);
|
key: 'installs'));
|
||||||
|
defaultValues
|
||||||
|
.add(defaultValues.isEmpty ? 'true' : '');
|
||||||
|
}
|
||||||
|
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
||||||
|
formInputs.add(GeneratedFormItem(
|
||||||
|
label:
|
||||||
|
'Mark ${trackOnlyUpdateIdsAllOrSelected.length} Track-Only\nApp${trackOnlyUpdateIdsAllOrSelected.length == 1 ? '' : 's'} as Updated',
|
||||||
|
type: FormItemType.bool,
|
||||||
|
key: 'trackonlies'));
|
||||||
|
defaultValues
|
||||||
|
.add(defaultValues.isEmpty ? 'true' : '');
|
||||||
}
|
}
|
||||||
showDialog<List<String>?>(
|
showDialog<List<String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
|
var totalApps = existingUpdateIdsAllOrSelected
|
||||||
|
.length +
|
||||||
|
newInstallIdsAllOrSelected.length +
|
||||||
|
trackOnlyUpdateIdsAllOrSelected.length;
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title:
|
title:
|
||||||
'Install${selectedApps.isEmpty ? ' ' : ' Selected '}Apps?',
|
'Change $totalApps App${totalApps == 1 ? '' : 's'}',
|
||||||
message:
|
items: formInputs.map((e) => [e]).toList(),
|
||||||
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
defaultValues: defaultValues,
|
||||||
items: formInputs,
|
|
||||||
defaultValues: [
|
|
||||||
'true',
|
|
||||||
existingUpdateIdsAllOrSelected.isEmpty
|
|
||||||
? 'true'
|
|
||||||
: ''
|
|
||||||
],
|
|
||||||
initValid: true,
|
initValid: true,
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
if (values.isEmpty) {
|
if (values.isEmpty) {
|
||||||
values = ['true', 'true'];
|
values = defaultValues;
|
||||||
}
|
}
|
||||||
bool shouldInstallUpdates = values[0] == 'true';
|
bool shouldInstallUpdates =
|
||||||
bool shouldInstallNew = values[1] == 'true';
|
findGeneratedFormValueByKey(
|
||||||
|
formInputs, values, 'updates') ==
|
||||||
|
'true';
|
||||||
|
bool shouldInstallNew =
|
||||||
|
findGeneratedFormValueByKey(
|
||||||
|
formInputs, values, 'installs') ==
|
||||||
|
'true';
|
||||||
|
bool shouldMarkTrackOnlies =
|
||||||
|
findGeneratedFormValueByKey(formInputs,
|
||||||
|
values, 'trackonlies') ==
|
||||||
|
'true';
|
||||||
settingsProvider
|
settingsProvider
|
||||||
.getInstallPermission()
|
.getInstallPermission()
|
||||||
.then((_) {
|
.then((_) {
|
||||||
@ -405,6 +440,10 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
toInstall
|
toInstall
|
||||||
.addAll(newInstallIdsAllOrSelected);
|
.addAll(newInstallIdsAllOrSelected);
|
||||||
}
|
}
|
||||||
|
if (shouldMarkTrackOnlies) {
|
||||||
|
toInstall.addAll(
|
||||||
|
trackOnlyUpdateIdsAllOrSelected);
|
||||||
|
}
|
||||||
appsProvider
|
appsProvider
|
||||||
.downloadAndInstallLatestApps(
|
.downloadAndInstallLatestApps(
|
||||||
toInstall, context)
|
toInstall, context)
|
||||||
|
@ -42,7 +42,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
|
|
||||||
Future<List<List<String>>> addApps(List<String> urls) async {
|
Future<List<List<String>>> addApps(List<String> urls) async {
|
||||||
await settingsProvider.getInstallPermission();
|
await settingsProvider.getInstallPermission();
|
||||||
List<dynamic> results = await sourceProvider.getApps(urls,
|
List<dynamic> results = await sourceProvider.getAppsByURLNaive(urls,
|
||||||
ignoreUrls: appsProvider.apps.values.map((e) => e.app.url).toList());
|
ignoreUrls: appsProvider.apps.values.map((e) => e.app.url).toList());
|
||||||
List<App> apps = results[0];
|
List<App> apps = results[0];
|
||||||
Map<String, dynamic> errorsMap = results[1];
|
Map<String, dynamic> errorsMap = results[1];
|
||||||
|
@ -139,18 +139,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
||||||
if (e.moreSourceSettingsFormItems.isNotEmpty) {
|
if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) {
|
||||||
return GeneratedForm(
|
return GeneratedForm(
|
||||||
items: e.moreSourceSettingsFormItems.map((e) => [e]).toList(),
|
items: e.additionalSourceSpecificSettingFormItems
|
||||||
|
.map((e) => [e])
|
||||||
|
.toList(),
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
for (var i = 0; i < values.length; i++) {
|
for (var i = 0; i < values.length; i++) {
|
||||||
settingsProvider.setSettingString(
|
settingsProvider.setSettingString(
|
||||||
e.moreSourceSettingsFormItems[i].id, values[i]);
|
e.additionalSourceSpecificSettingFormItems[i].id,
|
||||||
|
values[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultValues: e.moreSourceSettingsFormItems.map((e) {
|
defaultValues: e.additionalSourceSpecificSettingFormItems.map((e) {
|
||||||
return settingsProvider.getSettingString(e.id) ?? '';
|
return settingsProvider.getSettingString(e.id) ?? '';
|
||||||
}).toList());
|
}).toList());
|
||||||
} else {
|
} else {
|
||||||
|
@ -266,6 +266,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
Future<List<String>> downloadAndInstallLatestApps(
|
Future<List<String>> downloadAndInstallLatestApps(
|
||||||
List<String> appIds, BuildContext? context) async {
|
List<String> appIds, BuildContext? context) async {
|
||||||
List<String> appsToInstall = [];
|
List<String> appsToInstall = [];
|
||||||
|
List<String> trackOnlyAppsToUpdate = [];
|
||||||
// For all specified Apps, filter out those for which:
|
// For all specified Apps, filter out those for which:
|
||||||
// 1. A URL cannot be picked
|
// 1. A URL cannot be picked
|
||||||
// 2. That cannot be installed silently (IF no buildContext was given for interactive install)
|
// 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) {
|
if (apps[id] == null) {
|
||||||
throw ObtainiumError('App not found');
|
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) {
|
if (apkUrl != null) {
|
||||||
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
||||||
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
||||||
@ -284,7 +288,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
appsToInstall.add(id);
|
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
|
// Download APKs for all Apps to be installed
|
||||||
MultiAppMultiError errors = MultiAppMultiError();
|
MultiAppMultiError errors = MultiAppMultiError();
|
||||||
List<DownloadedApk?> downloadedFiles =
|
List<DownloadedApk?> downloadedFiles =
|
||||||
@ -391,7 +404,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return null; // Can't correct in the background isolate
|
return null; // Can't correct in the background isolate
|
||||||
}
|
}
|
||||||
var modded = false;
|
var modded = false;
|
||||||
if (installedInfo == null && app.installedVersion != null) {
|
if (installedInfo == null &&
|
||||||
|
app.installedVersion != null &&
|
||||||
|
!app.trackOnly) {
|
||||||
app.installedVersion = null;
|
app.installedVersion = null;
|
||||||
modded = true;
|
modded = true;
|
||||||
}
|
}
|
||||||
@ -445,8 +460,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
var info = await getInstalledInfo(newApps[i].id);
|
var info = await getInstalledInfo(newApps[i].id);
|
||||||
try {
|
try {
|
||||||
sp.getSource(newApps[i].url);
|
sp.getSource(newApps[i].url);
|
||||||
apps.putIfAbsent(
|
apps[newApps[i].id] = AppInMemory(newApps[i], null, info);
|
||||||
newApps[i].id, () => AppInMemory(newApps[i], null, info));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add([newApps[i].id, newApps[i].name, e.toString()]);
|
errors.add([newApps[i].id, newApps[i].name, e.toString()]);
|
||||||
}
|
}
|
||||||
@ -512,8 +526,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
currentApp.additionalData,
|
currentApp.additionalData,
|
||||||
name: currentApp.name,
|
name: currentApp.name,
|
||||||
id: currentApp.id,
|
id: currentApp.id,
|
||||||
pinned: currentApp.pinned);
|
pinned: currentApp.pinned,
|
||||||
newApp.installedVersion = currentApp.installedVersion;
|
trackOnly: currentApp.trackOnly,
|
||||||
|
installedVersion: currentApp.installedVersion);
|
||||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||||
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@ class App {
|
|||||||
late List<String> additionalData;
|
late List<String> additionalData;
|
||||||
late DateTime? lastUpdateCheck;
|
late DateTime? lastUpdateCheck;
|
||||||
bool pinned = false;
|
bool pinned = false;
|
||||||
|
bool trackOnly = false;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -53,7 +54,8 @@ class App {
|
|||||||
this.preferredApkIndex,
|
this.preferredApkIndex,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
this.lastUpdateCheck,
|
this.lastUpdateCheck,
|
||||||
this.pinned);
|
this.pinned,
|
||||||
|
this.trackOnly);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -74,12 +76,15 @@ class App {
|
|||||||
: List<String>.from(jsonDecode(json['apkUrls'])),
|
: List<String>.from(jsonDecode(json['apkUrls'])),
|
||||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
||||||
json['additionalData'] == null
|
json['additionalData'] == null
|
||||||
? SourceProvider().getSource(json['url']).additionalDataDefaults
|
? SourceProvider()
|
||||||
|
.getSource(json['url'])
|
||||||
|
.additionalSourceAppSpecificDefaults
|
||||||
: List<String>.from(jsonDecode(json['additionalData'])),
|
: List<String>.from(jsonDecode(json['additionalData'])),
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||||
json['pinned'] ?? false);
|
json['pinned'] ?? false,
|
||||||
|
json['trackOnly'] ?? false);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@ -92,7 +97,8 @@ class App {
|
|||||||
'preferredApkIndex': preferredApkIndex,
|
'preferredApkIndex': preferredApkIndex,
|
||||||
'additionalData': jsonEncode(additionalData),
|
'additionalData': jsonEncode(additionalData),
|
||||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||||
'pinned': pinned
|
'pinned': pinned,
|
||||||
|
'trackOnly': trackOnly
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +141,7 @@ List<String> getLinksFromParsedHTML(
|
|||||||
|
|
||||||
class AppSource {
|
class AppSource {
|
||||||
late String host;
|
late String host;
|
||||||
|
bool enforceTrackOnly = false;
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
@ -148,9 +155,22 @@ class AppSource {
|
|||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
// Different Sources may need different kinds of additional data for Apps
|
||||||
List<String> additionalDataDefaults = [];
|
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = [];
|
||||||
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
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) {
|
String? changeLogPageFromStandardUrl(String standardUrl) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
@ -211,7 +231,7 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ifSourceAppsRequireAdditionalData(AppSource source) {
|
bool ifSourceAppsRequireAdditionalData(AppSource source) {
|
||||||
for (var row in source.additionalDataFormItems) {
|
for (var row in source.additionalSourceAppSpecificFormItems) {
|
||||||
for (var element in row) {
|
for (var element in row) {
|
||||||
if (element.required) {
|
if (element.required) {
|
||||||
return true;
|
return true;
|
||||||
@ -238,11 +258,19 @@ class SourceProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
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));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
AppNames names = source.getAppNames(standardUrl);
|
AppNames names = source.getAppNames(standardUrl);
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalData);
|
await source.getLatestAPKDetails(standardUrl, additionalData);
|
||||||
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
String apkVersion = apk.version.replaceAll('/', '-');
|
||||||
return App(
|
return App(
|
||||||
id ??
|
id ??
|
||||||
source.tryInferringAppId(standardUrl) ??
|
source.tryInferringAppId(standardUrl) ??
|
||||||
@ -252,24 +280,26 @@ class SourceProvider {
|
|||||||
name.trim().isNotEmpty
|
name.trim().isNotEmpty
|
||||||
? name
|
? name
|
||||||
: names.name[0].toUpperCase() + names.name.substring(1),
|
: names.name[0].toUpperCase() + names.name.substring(1),
|
||||||
null,
|
installedVersion,
|
||||||
apk.version.replaceAll('/', '-'),
|
apkVersion,
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
apk.apkUrls.length - 1,
|
apk.apkUrls.length - 1,
|
||||||
additionalData,
|
additionalData,
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
pinned);
|
pinned,
|
||||||
|
trackOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// 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<String> ignoreUrls = const []}) async {
|
||||||
List<App> apps = [];
|
List<App> apps = [];
|
||||||
Map<String, dynamic> errors = {};
|
Map<String, dynamic> errors = {};
|
||||||
for (var url in urls.where((element) => !ignoreUrls.contains(element))) {
|
for (var url in urls.where((element) => !ignoreUrls.contains(element))) {
|
||||||
try {
|
try {
|
||||||
var source = getSource(url);
|
var source = getSource(url);
|
||||||
apps.add(await getApp(source, url, source.additionalDataDefaults));
|
apps.add(await getApp(
|
||||||
|
source, url, source.additionalSourceAppSpecificDefaults));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.addAll(<String, dynamic>{url: e});
|
errors.addAll(<String, dynamic>{url: e});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user