This commit is contained in:
Imran Remtulla
2022-12-20 18:00:22 -05:00
parent 9a129d41df
commit 6a21045e5b
16 changed files with 181 additions and 173 deletions

View File

@ -25,8 +25,9 @@ class APKMirror extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/feed')); Response res = await get(Uri.parse('$standardUrl/feed'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
String? titleString = parse(res.body) String? titleString = parse(res.body)

View File

@ -32,7 +32,7 @@ class FDroid extends AppSource {
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{Map<String, String> additionalData = const {}}) { {Map<String, String> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last; return Uri.parse(standardUrl).pathSegments.last;
} }
@ -60,8 +60,9 @@ class FDroid extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),

View File

@ -9,7 +9,7 @@ class FDroidRepo extends AppSource {
FDroidRepo() { FDroidRepo() {
name = tr('fdroidThirdPartyRepo'); name = tr('fdroidThirdPartyRepo');
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem('appIdOrName', GeneratedFormItem('appIdOrName',
label: tr('appIdOrName'), label: tr('appIdOrName'),
@ -32,9 +32,10 @@ class FDroidRepo extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
String? appIdOrName = additionalData['appIdOrName']; ) async {
String? appIdOrName = additionalSettings['appIdOrName'];
if (appIdOrName == null) { if (appIdOrName == null) {
throw NoReleasesError(); throw NoReleasesError();
} }

View File

@ -49,7 +49,7 @@ class GitHub extends AppSource {
]) ])
]; ];
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem('includePrereleases', GeneratedFormItem('includePrereleases',
label: tr('includePrereleases'), label: tr('includePrereleases'),
@ -110,14 +110,15 @@ class GitHub extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
var includePrereleases = additionalData['includePrereleases'] == 'true'; ) async {
var includePrereleases = additionalSettings['includePrereleases'] == 'true';
var fallbackToOlderReleases = var fallbackToOlderReleases =
additionalData['fallbackToOlderReleases'] == 'true'; additionalSettings['fallbackToOlderReleases'] == 'true';
var regexFilter = var regexFilter =
additionalData['filterReleaseTitlesByRegEx']?.isNotEmpty == true additionalSettings['filterReleaseTitlesByRegEx']?.isNotEmpty == true
? additionalData['filterReleaseTitlesByRegEx'] ? additionalSettings['filterReleaseTitlesByRegEx']
: null; : null;
Response res = await get(Uri.parse( Response res = await get(Uri.parse(
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases')); 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
@ -149,7 +150,7 @@ class GitHub extends AppSource {
continue; continue;
} }
var apkUrls = getReleaseAPKUrls(releases[i]); var apkUrls = getReleaseAPKUrls(releases[i]);
if (apkUrls.isEmpty && !trackOnly) { if (apkUrls.isEmpty && additionalSettings['trackOnly'] != 'true') {
continue; continue;
} }
targetRelease = releases[i]; targetRelease = releases[i];

View File

@ -25,8 +25,9 @@ class GitLab extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl); var standardUri = Uri.parse(standardUrl);

View File

@ -23,14 +23,15 @@ class IzzyOnDroid extends AppSource {
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{Map<String, String> additionalData = const {}}) { {Map<String, String> additionalSettings = const {}}) {
return FDroid().tryInferringAppId(standardUrl); return FDroid().tryInferringAppId(standardUrl);
} }
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
await get( await get(

View File

@ -24,8 +24,9 @@ class Mullvad extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/en/download/android')); Response res = await get(Uri.parse('$standardUrl/en/download/android'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var version = parse(res.body) var version = parse(res.body)

View File

@ -18,8 +18,9 @@ class Signal extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = Response res =
await get(Uri.parse('https://updates.$host/android/latest.json')); await get(Uri.parse('https://updates.$host/android/latest.json'));
if (res.statusCode == 200) { if (res.statusCode == 200) {

View File

@ -23,8 +23,9 @@ class SourceForge extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/rss?path=/')); Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);

View File

@ -9,7 +9,7 @@ class SteamMobile extends AppSource {
SteamMobile() { SteamMobile() {
host = 'store.steampowered.com'; host = 'store.steampowered.com';
name = tr('steam'); name = tr('steam');
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem('app', GeneratedFormItem('app',
label: tr('app'), required: true, opts: apks.entries.toList()) label: tr('app'), required: true, opts: apks.entries.toList())
@ -29,11 +29,12 @@ class SteamMobile extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, String> additionalSettings,
) async {
Response res = await get(Uri.parse('https://$host/mobile')); Response res = await get(Uri.parse('https://$host/mobile'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var apkNamePrefix = additionalData['app']; var apkNamePrefix = additionalSettings['app'];
if (apkNamePrefix == null) { if (apkNamePrefix == null) {
throw NoReleasesError(); throw NoReleasesError();
} }

View File

@ -202,7 +202,6 @@ class _ObtainiumState extends State<Obtainium> {
0, 0,
{}, {},
null, null,
false,
false) false)
]); ]);
} }

View File

@ -27,10 +27,8 @@ class _AddAppPageState extends State<AddAppPage> {
String userInput = ''; String userInput = '';
String searchQuery = ''; String searchQuery = '';
AppSource? pickedSource; AppSource? pickedSource;
Map<String, String> sourceSpecificAdditionalData = {}; Map<String, String> additionalSettings = {};
bool sourceSpecificDataIsValid = true; bool additionalSettingsValid = true;
Map<String, String> otherAdditionalData = {};
bool otherAdditionalDataIsValid = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -43,12 +41,12 @@ class _AddAppPageState extends State<AddAppPage> {
var source = valid ? sourceProvider.getSource(userInput) : null; var source = valid ? sourceProvider.getSource(userInput) : null;
if (pickedSource.runtimeType != source.runtimeType) { if (pickedSource.runtimeType != source.runtimeType) {
pickedSource = source; pickedSource = source;
sourceSpecificAdditionalData = source != null additionalSettings = source != null
? getDefaultValuesFromFormItems( ? getDefaultValuesFromFormItems(
source.additionalSourceAppSpecificFormItems) source.combinedAppSpecificSettingFormItems)
: {}; : {};
sourceSpecificDataIsValid = source != null additionalSettingsValid = source != null
? !sourceProvider.ifSourceAppsRequireAdditionalData(source) ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true; : true;
} }
} }
@ -68,10 +66,9 @@ class _AddAppPageState extends State<AddAppPage> {
}); });
var settingsProvider = context.read<SettingsProvider>(); var settingsProvider = context.read<SettingsProvider>();
() async { () async {
var userPickedTrackOnly = var userPickedTrackOnly = additionalSettings['trackOnly'] == 'true';
otherAdditionalData['trackOnlyFormItemKey'] == 'true';
var userPickedNoVersionDetection = var userPickedNoVersionDetection =
otherAdditionalData['noVersionDetectionKey'] == 'true'; additionalSettings['noVersionDetection'] == 'true';
var cont = true; var cont = true;
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
await showDialog( await showDialog(
@ -108,14 +105,15 @@ class _AddAppPageState extends State<AddAppPage> {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
App app = await sourceProvider.getApp( App app = await sourceProvider.getApp(
pickedSource!, userInput, sourceSpecificAdditionalData, pickedSource!, userInput, additionalSettings,
trackOnlyOverride: trackOnly, trackOnlyOverride: trackOnly,
noVersionDetectionOverride: userPickedNoVersionDetection); noVersionDetectionOverride: userPickedNoVersionDetection);
if (!trackOnly) { if (!trackOnly) {
await settingsProvider.getInstallPermission(); await settingsProvider.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.isTempId(app.id) && !app.trackOnly) { if (sourceProvider.isTempId(app.id) &&
app.additionalSettings['trackOnly'] != 'true') {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
var apkUrl = await appsProvider.confirmApkUrl(app, context); var apkUrl = await appsProvider.confirmApkUrl(app, context);
if (apkUrl == null) { if (apkUrl == null) {
@ -130,7 +128,7 @@ class _AddAppPageState extends State<AddAppPage> {
if (appsProvider.apps.containsKey(app.id)) { if (appsProvider.apps.containsKey(app.id)) {
throw ObtainiumError(tr('appAlreadyAdded')); throw ObtainiumError(tr('appAlreadyAdded'));
} }
if (app.trackOnly) { if (app.additionalSettings['trackOnly'] == 'true') {
app.installedVersion = app.latestVersion; app.installedVersion = app.latestVersion;
} }
await appsProvider.saveApps([app]); await appsProvider.saveApps([app]);
@ -206,13 +204,9 @@ class _AddAppPageState extends State<AddAppPage> {
onPressed: gettingAppInfo || onPressed: gettingAppInfo ||
pickedSource == null || pickedSource == null ||
(pickedSource! (pickedSource!
.additionalSourceAppSpecificFormItems .combinedAppSpecificSettingFormItems
.isNotEmpty && .isNotEmpty &&
!sourceSpecificDataIsValid) || !additionalSettingsValid)
(pickedSource!
.additionalAppSpecificSourceAgnosticFormItems
.isNotEmpty &&
!otherAdditionalDataIsValid)
? null ? null
: addApp, : addApp,
child: Text(tr('add'))) child: Text(tr('add')))
@ -305,15 +299,8 @@ class _AddAppPageState extends State<AddAppPage> {
], ],
), ),
if (pickedSource != null && if (pickedSource != null &&
(pickedSource!.additionalSourceAppSpecificFormItems (pickedSource!
.isNotEmpty || .combinedAppSpecificSettingFormItems.isNotEmpty))
pickedSource!
.additionalAppSpecificSourceAgnosticFormItems
.where((e) => pickedSource!.enforceTrackOnly
? e.key != 'trackOnlyFormItemKey'
: true)
.map((e) => [e])
.isNotEmpty))
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -329,39 +316,14 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
if (pickedSource!
.additionalSourceAppSpecificFormItems
.isNotEmpty)
GeneratedForm(
items: pickedSource!
.additionalSourceAppSpecificFormItems,
onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) {
setState(() {
sourceSpecificAdditionalData = values;
sourceSpecificDataIsValid = valid;
});
}
}),
if (pickedSource!
.additionalAppSpecificSourceAgnosticFormItems
.isNotEmpty)
const SizedBox(
height: 8,
),
GeneratedForm( GeneratedForm(
items: pickedSource! items: pickedSource!
.additionalAppSpecificSourceAgnosticFormItems .combinedAppSpecificSettingFormItems,
.where((e) => pickedSource!.enforceTrackOnly
? e.key != 'trackOnlyFormItemKey'
: true)
.map((e) => [e])
.toList(),
onValueChanges: (values, valid, isBuilding) { onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) { if (!isBuilding) {
setState(() { setState(() {
otherAdditionalData = values; additionalSettings = values;
otherAdditionalDataIsValid = valid; additionalSettingsValid = valid;
}); });
} }
}), }),

View File

@ -40,6 +40,7 @@ class _AppPageState extends State<AppPage> {
prevApp = app; prevApp = app;
getUpdate(app.app.id); getUpdate(app.app.id);
} }
var trackOnly = app?.app.additionalSettings['trackOnly'] == 'true';
return Scaffold( return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null, appBar: settingsProvider.showAppWebpage ? AppBar() : null,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
@ -111,7 +112,7 @@ class _AppPageState extends State<AppPage> {
Text( Text(
'${tr('installedVersionX', args: [ '${tr('installedVersionX', args: [
app?.app.installedVersion ?? tr('none') app?.app.installedVersion ?? tr('none')
])}${app?.app.trackOnly == true ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
tr('app') tr('app')
])}' : ''}', ])}' : ''}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -151,7 +152,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 && !trackOnly &&
app?.app.installedVersion != app?.app.latestVersion) app?.app.installedVersion != app?.app.latestVersion)
IconButton( IconButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null
@ -202,8 +203,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.additionalSourceAppSpecificFormItems source
.isNotEmpty) .combinedAppSpecificSettingFormItems.isNotEmpty)
IconButton( IconButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null
? null ? null
@ -211,14 +212,36 @@ class _AppPageState extends State<AppPage> {
showDialog<Map<String, String>>( showDialog<Map<String, String>>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
var items = source
.combinedAppSpecificSettingFormItems
.map((row) {
row.map((e) {
if (app?.app.additionalSettings[
e.key] !=
null) {
e.defaultValue = app?.app
.additionalSettings[
e.key];
}
return e;
}).toList();
return row;
}).toList();
return GeneratedFormModal( return GeneratedFormModal(
title: 'Additional Options', title: 'Additional Options',
items: source items: items);
.additionalSourceAppSpecificFormItems);
}).then((values) { }).then((values) {
if (app != null && values != null) { if (app != null && values != null) {
var changedApp = app.app; var changedApp = app.app;
changedApp.additionalData = values; changedApp.additionalSettings =
values;
if (source.enforceTrackOnly) {
changedApp.additionalSettings[
'trackOnly'] = 'true';
showError(
tr('appsFromSourceAreTrackOnly'),
context);
}
appsProvider.saveApps( appsProvider.saveApps(
[changedApp]).then((value) { [changedApp]).then((value) {
getUpdate(changedApp.id); getUpdate(changedApp.id);
@ -238,7 +261,9 @@ class _AppPageState extends State<AppPage> {
? () { ? () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
() async { () async {
if (app?.app.trackOnly != true) { if (app?.app.additionalSettings[
'trackOnly'] !=
'true') {
await settingsProvider await settingsProvider
.getInstallPermission(); .getInstallPermission();
} }
@ -260,10 +285,10 @@ class _AppPageState extends State<AppPage> {
} }
: null, : null,
child: Text(app?.app.installedVersion == null child: Text(app?.app.installedVersion == null
? app?.app.trackOnly == false ? !trackOnly
? 'Install' ? 'Install'
: 'Mark Installed' : 'Mark Installed'
: app?.app.trackOnly == false : !trackOnly
? 'Update' ? 'Update'
: 'Mark Updated'))), : 'Mark Updated'))),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),

View File

@ -139,14 +139,16 @@ class AppsPageState extends State<AppsPage> {
List<String> trackOnlyUpdateIdsAllOrSelected = []; List<String> trackOnlyUpdateIdsAllOrSelected = [];
existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) { existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
if (appsProvider.apps[id]!.app.trackOnly) { if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] ==
'true') {
trackOnlyUpdateIdsAllOrSelected.add(id); trackOnlyUpdateIdsAllOrSelected.add(id);
return false; return false;
} }
return true; return true;
}).toList(); }).toList();
newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) { newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
if (appsProvider.apps[id]!.app.trackOnly) { if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] ==
'true') {
trackOnlyUpdateIdsAllOrSelected.add(id); trackOnlyUpdateIdsAllOrSelected.add(id);
return false; return false;
} }
@ -271,7 +273,7 @@ class AppsPageState extends State<AppsPage> {
SizedBox( SizedBox(
width: 100, width: 100,
child: Text( child: Text(
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.trackOnly == true ? ' ${tr('estimateInBrackets')}' : ''}', '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}',
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
textAlign: TextAlign.end, textAlign: TextAlign.end,
)), )),
@ -289,7 +291,7 @@ class AppsPageState extends State<AppsPage> {
child: appsProvider.areDownloadsRunning() child: appsProvider.areDownloadsRunning()
? Text(tr('pleaseWait')) ? Text(tr('pleaseWait'))
: Text( : Text(
'${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}', '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}',
style: TextStyle( style: TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
decoration: changesUrl == null decoration: changesUrl == null
@ -343,7 +345,7 @@ class AppsPageState extends State<AppsPage> {
: IconButton( : IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: () {
showDialog<List<String>?>( showDialog<Map<String, String>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return GeneratedFormModal( return GeneratedFormModal(

View File

@ -313,7 +313,8 @@ class AppsProvider with ChangeNotifier {
throw ObtainiumError(tr('appNotFound')); throw ObtainiumError(tr('appNotFound'));
} }
String? apkUrl; String? apkUrl;
if (!apps[id]!.app.trackOnly) { var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == 'true';
if (!trackOnly) {
apkUrl = await confirmApkUrl(apps[id]!.app, context); apkUrl = await confirmApkUrl(apps[id]!.app, context);
} }
if (apkUrl != null) { if (apkUrl != null) {
@ -326,7 +327,7 @@ class AppsProvider with ChangeNotifier {
appsToInstall.add(id); appsToInstall.add(id);
} }
} }
if (apps[id]!.app.trackOnly) { if (trackOnly) {
trackOnlyAppsToUpdate.add(id); trackOnlyAppsToUpdate.add(id);
} }
} }
@ -451,9 +452,10 @@ class AppsProvider with ChangeNotifier {
// Don't save changes, just return the object if changes were made (else null) // Don't save changes, just return the object if changes were made (else null)
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
var modded = false; var modded = false;
if (installedInfo == null && var trackOnly = app.additionalSettings['trackOnly'] == 'true';
app.installedVersion != null && var noVersionDetection =
!app.trackOnly) { app.additionalSettings['noVersionDetection'] == 'true';
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
app.installedVersion = null; app.installedVersion = null;
modded = true; modded = true;
} else if (installedInfo?.versionName != null && } else if (installedInfo?.versionName != null &&
@ -462,7 +464,7 @@ class AppsProvider with ChangeNotifier {
modded = true; modded = true;
} else if (installedInfo?.versionName != null && } else if (installedInfo?.versionName != null &&
installedInfo!.versionName != app.installedVersion && installedInfo!.versionName != app.installedVersion &&
!app.noVersionDetection) { !noVersionDetection) {
String? correctedInstalledVersion = reconcileRealAndInternalVersions( String? correctedInstalledVersion = reconcileRealAndInternalVersions(
installedInfo.versionName!, app.installedVersion!); installedInfo.versionName!, app.installedVersion!);
if (correctedInstalledVersion != null) { if (correctedInstalledVersion != null) {
@ -472,7 +474,7 @@ class AppsProvider with ChangeNotifier {
} }
if (app.installedVersion != null && if (app.installedVersion != null &&
app.installedVersion != app.latestVersion && app.installedVersion != app.latestVersion &&
!app.noVersionDetection) { !noVersionDetection) {
app.installedVersion = reconcileRealAndInternalVersions( app.installedVersion = reconcileRealAndInternalVersions(
app.installedVersion!, app.latestVersion, app.installedVersion!, app.latestVersion,
matchMode: true) ?? matchMode: true) ??
@ -625,7 +627,7 @@ class AppsProvider with ChangeNotifier {
App newApp = await sourceProvider.getApp( App newApp = await sourceProvider.getApp(
sourceProvider.getSource(currentApp.url), sourceProvider.getSource(currentApp.url),
currentApp.url, currentApp.url,
currentApp.additionalData, currentApp.additionalSettings,
currentApp: currentApp); currentApp: currentApp);
if (currentApp.preferredApkIndex < newApp.apkUrls.length) { if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
newApp.preferredApkIndex = currentApp.preferredApkIndex; newApp.preferredApkIndex = currentApp.preferredApkIndex;

View File

@ -44,11 +44,9 @@ class App {
late String latestVersion; late String latestVersion;
List<String> apkUrls = []; List<String> apkUrls = [];
late int preferredApkIndex; late int preferredApkIndex;
late Map<String, String> additionalData; late Map<String, String> additionalSettings;
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
bool pinned = false; bool pinned = false;
bool trackOnly = false;
bool noVersionDetection = false;
App( App(
this.id, this.id,
this.url, this.url,
@ -58,38 +56,37 @@ class App {
this.latestVersion, this.latestVersion,
this.apkUrls, this.apkUrls,
this.preferredApkIndex, this.preferredApkIndex,
this.additionalData, this.additionalSettings,
this.lastUpdateCheck, this.lastUpdateCheck,
this.pinned, this.pinned);
this.trackOnly,
{this.noVersionDetection = false});
@override @override
String toString() { String toString() {
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
} }
factory App.fromJson(Map<String, dynamic> json) { factory App.fromJson(Map<String, dynamic> json) {
var formItems = SourceProvider() var source = SourceProvider().getSource(json['url']);
.getSource(json['url']) var formItems = source.combinedAppSpecificSettingFormItems
.additionalSourceAppSpecificFormItems
.reduce((value, element) => [...value, ...element]); .reduce((value, element) => [...value, ...element]);
Map<String, String> additionalData = Map<String, String> additionalSettings =
getDefaultValuesFromFormItems([formItems]); getDefaultValuesFromFormItems([formItems]);
if (json['additionalSettings'] != null) {
additionalSettings.addEntries(
Map<String, String>.from(jsonDecode(json['additionalSettings']))
.entries);
}
// If needed, migrate old-style additionalData to new-style additionalSettings
if (json['additionalData'] != null) { if (json['additionalData'] != null) {
try { List<String> temp = List<String>.from(jsonDecode(json['additionalData']));
additionalData = temp.asMap().forEach((i, value) {
Map<String, String>.from(jsonDecode(json['additionalData'])); if (i < formItems.length) {
} catch (e) { additionalSettings[formItems[i].key] = value;
// Migrate old-style additionalData List to new-style Map }
List<String> temp = });
List<String>.from(jsonDecode(json['additionalData'])); additionalSettings['trackOnly'] = (json['trackOnly'] ?? false).toString();
temp.asMap().forEach((i, value) { additionalSettings['noVersionDetection'] =
if (i < formItems.length) { (json['noVersionDetection'] ?? false).toString();
additionalData[formItems[i].key] = value;
}
});
}
} }
return App( return App(
json['id'] as String, json['id'] as String,
@ -106,13 +103,11 @@ class App {
json['preferredApkIndex'] == null json['preferredApkIndex'] == null
? 0 ? 0
: json['preferredApkIndex'] as int, : json['preferredApkIndex'] as int,
additionalData, additionalSettings,
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,
noVersionDetection: json['noVersionDetection'] ?? false);
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@ -124,11 +119,9 @@ class App {
'latestVersion': latestVersion, 'latestVersion': latestVersion,
'apkUrls': jsonEncode(apkUrls), 'apkUrls': jsonEncode(apkUrls),
'preferredApkIndex': preferredApkIndex, 'preferredApkIndex': preferredApkIndex,
'additionalData': jsonEncode(additionalData), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned
'trackOnly': trackOnly,
'noVersionDetection': noVersionDetection
}; };
} }
@ -183,26 +176,39 @@ class AppSource {
} }
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, Map<String, String> additionalData, String standardUrl, Map<String, String> additionalSettings) {
{bool trackOnly = false}) {
throw NotImplementedError(); throw NotImplementedError();
} }
// Different Sources may need different kinds of additional data for Apps // Different Sources may need different kinds of additional data for Apps
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = []; List<List<GeneratedFormItem>> additionalSourceAppSpecificSettingFormItems =
[];
// Some additional data may be needed for Apps regardless of Source // Some additional data may be needed for Apps regardless of Source
final List<GeneratedFormItem> additionalAppSpecificSourceAgnosticFormItems = [ final List<List<GeneratedFormItem>>
GeneratedFormItem( additionalAppSpecificSourceAgnosticSettingFormItems = [
'trackOnlyFormItemKey', [
label: tr('trackOnly'), GeneratedFormItem(
type: FormItemType.bool, 'trackOnly',
), label: tr('trackOnly'),
GeneratedFormItem('noVersionDetectionKey', type: FormItemType.bool,
label: 'Do not attempt version detection', // TODO )
type: FormItemType.bool) ],
[
GeneratedFormItem('noVersionDetection',
label: 'Do not attempt version detection', // TODO
type: FormItemType.bool)
]
]; ];
// Previous 2 variables combined into one at runtime for convenient usage
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
return [
...additionalSourceAppSpecificSettingFormItems,
...additionalAppSpecificSourceAgnosticSettingFormItems
];
}
// Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider // Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = []; List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
@ -220,7 +226,7 @@ class AppSource {
} }
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{Map<String, String> additionalData = const {}}) { {Map<String, String> additionalSettings = const {}}) {
return null; return null;
} }
} }
@ -280,8 +286,8 @@ class SourceProvider {
return source; return source;
} }
bool ifSourceAppsRequireAdditionalData(AppSource source) { bool ifRequiredAppSpecificSettingsExist(AppSource source) {
for (var row in source.additionalSourceAppSpecificFormItems) { for (var row in source.combinedAppSpecificSettingFormItems) {
for (var element in row) { for (var element in row) {
if (element.required && element.opts == null) { if (element.required && element.opts == null) {
return true; return true;
@ -309,15 +315,20 @@ class SourceProvider {
} }
Future<App> getApp( Future<App> getApp(
AppSource source, String url, Map<String, String> additionalData, AppSource source, String url, Map<String, String> additionalSettings,
{App? currentApp, {App? currentApp,
bool trackOnlyOverride = false, bool trackOnlyOverride = false,
noVersionDetectionOverride = false}) async { noVersionDetectionOverride = false}) async {
var trackOnly = trackOnlyOverride || (currentApp?.trackOnly ?? false); if (trackOnlyOverride) {
additionalSettings['trackOnly'] = 'true';
}
if (noVersionDetectionOverride) {
additionalSettings['noVersionDetection'] = 'true';
}
var trackOnly = currentApp?.additionalSettings['trackOnly'] == 'true';
String standardUrl = source.standardizeURL(preStandardizeUrl(url)); String standardUrl = source.standardizeURL(preStandardizeUrl(url));
APKDetails apk = await source APKDetails apk =
.getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly); await source.getLatestAPKDetails(standardUrl, additionalSettings);
if (apk.apkUrls.isEmpty && !trackOnly) { if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError(); throw NoAPKError();
} }
@ -327,7 +338,7 @@ class SourceProvider {
return App( return App(
currentApp?.id ?? currentApp?.id ??
source.tryInferringAppId(standardUrl, source.tryInferringAppId(standardUrl,
additionalData: additionalData) ?? additionalSettings: additionalSettings) ??
generateTempID(apk.names, source), generateTempID(apk.names, source),
standardUrl, standardUrl,
apk.names.author[0].toUpperCase() + apk.names.author.substring(1), apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
@ -338,12 +349,9 @@ class SourceProvider {
apkVersion, apkVersion,
apk.apkUrls, apk.apkUrls,
apk.apkUrls.length - 1, apk.apkUrls.length - 1,
additionalData, additionalSettings,
DateTime.now(), DateTime.now(),
currentApp?.pinned ?? false, currentApp?.pinned ?? false);
trackOnly,
noVersionDetection: noVersionDetectionOverride ||
(currentApp?.noVersionDetection ?? false));
} }
// Returns errors in [results, errors] instead of throwing them // Returns errors in [results, errors] instead of throwing them
@ -358,7 +366,7 @@ class SourceProvider {
source, source,
url, url,
getDefaultValuesFromFormItems( getDefaultValuesFromFormItems(
source.additionalSourceAppSpecificFormItems))); source.combinedAppSpecificSettingFormItems)));
} catch (e) { } catch (e) {
errors.addAll(<String, dynamic>{url: e}); errors.addAll(<String, dynamic>{url: e});
} }