From 9fba7478023691d4e1a7720d86f4a32699acf9ca Mon Sep 17 00:00:00 2001 From: Imran Remtulla <30463115+ImranR98@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:27:36 -0400 Subject: [PATCH] Version detection improvements, Mullvad web scraping fix and changelog addition, code readability improvements, general tweaks/bugfixes (#400) 1. Apps that don't have "standard" versioning formats now automatically stop using version detection. This will prevent users from having to learn about this feature and enable it manually. - For such Apps, the "standard" version detection option is greyed out. 2. The Mullvad Source recently broke due to a slight change in their website design. This is now fixed. - Mullvad also now provides an in-app changelog via their official GitHub repo. 3. Code has been refactored for readability (specifically the version detection code and UI code for most screens). 4. Minor UI tweaks and bugfixes. --- lib/app_sources/mullvad.dart | 37 +- lib/components/generated_form.dart | 15 +- lib/main.dart | 2 +- lib/pages/add_app.dart | 544 +++++----- lib/pages/app.dart | 787 +++++++------- lib/pages/apps.dart | 1528 +++++++++++++--------------- lib/pages/import_export.dart | 486 ++++----- lib/providers/apps_provider.dart | 143 ++- lib/providers/source_provider.dart | 21 +- pubspec.lock | 28 +- pubspec.yaml | 2 +- 11 files changed, 1720 insertions(+), 1873 deletions(-) diff --git a/lib/app_sources/mullvad.dart b/lib/app_sources/mullvad.dart index 0d8fed9..a7449dd 100644 --- a/lib/app_sources/mullvad.dart +++ b/lib/app_sources/mullvad.dart @@ -1,5 +1,7 @@ import 'package:html/parser.dart'; import 'package:http/http.dart'; +import 'package:obtainium/app_sources/github.dart'; +import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; @@ -27,23 +29,24 @@ class Mullvad extends AppSource { String standardUrl, Map additionalSettings, ) async { - Response res = await get(Uri.parse('$standardUrl/en/download/android')); - if (res.statusCode == 200) { - var version = parse(res.body) - .querySelector('p.subtitle.is-6') - ?.querySelector('a') - ?.attributes['href'] - ?.split('/') - .last; - if (version == null) { - throw NoVersionError(); - } - return APKDetails( - version, - ['https://mullvad.net/download/app/apk/latest'], - AppNames(name, 'Mullvad-VPN')); - } else { - throw getObtainiumHttpError(res); + var details = await HTML().getLatestAPKDetails( + '$standardUrl/en/download/android', additionalSettings); + var fileName = details.apkUrls[0].split('/').last; + var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(fileName); + if (versionMatch == null) { + throw NoVersionError(); } + details.version = fileName.substring(versionMatch.start, versionMatch.end); + details.names = AppNames(name, 'Mullvad-VPN'); + try { + details.changeLog = (await GitHub().getLatestAPKDetails( + 'https://github.com/mullvad/mullvadvpn-app', + {'fallbackToOlderReleases': true})) + .changeLog; + } catch (e) { + print(e); + // Ignore + } + return details; } } diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index a281899..3f892c8 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -48,6 +48,7 @@ class GeneratedFormTextField extends GeneratedFormItem { class GeneratedFormDropdown extends GeneratedFormItem { late List>? opts; + List? disabledOptKeys; GeneratedFormDropdown( String key, @@ -55,6 +56,7 @@ class GeneratedFormDropdown extends GeneratedFormItem { String label = 'Input', List belowWidgets = const [], String defaultValue = '', + this.disabledOptKeys, List additionalValidators = const [], }) : super(key, label: label, @@ -225,10 +227,15 @@ class _GeneratedFormState extends State { return DropdownButtonFormField( decoration: InputDecoration(labelText: formItem.label), value: values[formItem.key], - items: formItem.opts! - .map((e2) => - DropdownMenuItem(value: e2.key, child: Text(e2.value))) - .toList(), + items: formItem.opts!.map((e2) { + var enabled = + formItem.disabledOptKeys?.contains(e2.key) != true; + return DropdownMenuItem( + value: e2.key, + enabled: enabled, + child: Opacity( + opacity: enabled ? 1 : 0.5, child: Text(e2.value))); + }).toList(), onChanged: (value) { setState(() { values[formItem.key] = value ?? formItem.opts!.first.key; diff --git a/lib/main.dart b/lib/main.dart index 8b3737a..02ce25a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.11.15'; +const String currentVersion = '0.11.16'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index a2e3cda..64d7f3c 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -33,10 +33,10 @@ class _AddAppPageState extends State { bool additionalSettingsValid = true; List pickedCategories = []; int searchnum = 0; + SourceProvider sourceProvider = SourceProvider(); @override Widget build(BuildContext context) { - SourceProvider sourceProvider = SourceProvider(); AppsProvider appsProvider = context.read(); bool doingSomething = gettingAppInfo || searching; @@ -64,65 +64,56 @@ class _AddAppPageState extends State { } } + getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async { + return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && + // ignore: use_build_context_synchronously + await showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('xIsTrackOnly', args: [ + pickedSource!.enforceTrackOnly + ? tr('source') + : tr('app') + ]), + items: const [], + message: + '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', + ); + }) == + null)); + } + + getReleaseDateAsVersionConfirmationIfNeeded( + bool userPickedTrackOnly) async { + return (!(additionalSettings['versionDetection'] == + 'releaseDateAsVersion' && + // ignore: use_build_context_synchronously + await showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('releaseDateAsVersion'), + items: const [], + message: tr('releaseDateAsVersionExplanation'), + ); + }) == + null)); + } + addApp({bool resetUserInputAfter = false}) async { setState(() { gettingAppInfo = true; }); - var settingsProvider = context.read(); - () async { + try { + var settingsProvider = context.read(); var userPickedTrackOnly = additionalSettings['trackOnly'] == true; - var cont = true; - if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && - // ignore: use_build_context_synchronously - await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('xIsTrackOnly', args: [ - pickedSource!.enforceTrackOnly - ? tr('source') - : tr('app') - ]), - items: const [], - message: - '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', - ); - }) == - null) { - cont = false; - } - if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' && - // ignore: use_build_context_synchronously - await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('releaseDateAsVersion'), - items: const [], - message: tr('releaseDateAsVersionExplanation'), - ); - }) == - null) { - cont = false; - } - if (additionalSettings['versionDetection'] == 'noVersionDetection' && - // ignore: use_build_context_synchronously - await showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('disableVersionDetection'), - items: const [], - message: tr('noVersionDetectionExplanation'), - ); - }) == - null) { - cont = false; - } - if (cont) { - HapticFeedback.selectionClick(); + App? app; + if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) && + (await getReleaseDateAsVersionConfirmationIfNeeded( + userPickedTrackOnly))) { var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; - App app = await sourceProvider.getApp( + app = await sourceProvider.getApp( pickedSource!, userInput, additionalSettings, trackOnlyOverride: trackOnly); if (!trackOnly) { @@ -150,27 +141,232 @@ class _AddAppPageState extends State { } app.categories = pickedCategories; await appsProvider.saveApps([app], onlyIfExists: false); - - return app; } - }() - .then((app) { if (app != null) { Navigator.push(globalNavigatorKey.currentContext ?? context, - MaterialPageRoute(builder: (context) => AppPage(appId: app.id))); + MaterialPageRoute(builder: (context) => AppPage(appId: app!.id))); } - }).catchError((e) { + } catch (e) { showError(e, context); - }).whenComplete(() { + } finally { setState(() { gettingAppInfo = false; if (resetUserInputAfter) { changeUserInput('', false, true); } }); - }); + } } + Widget getUrlInputRow() => Row( + children: [ + Expanded( + child: GeneratedForm( + key: Key(searchnum.toString()), + items: [ + [ + GeneratedFormTextField('appSourceURL', + label: tr('appSourceURL'), + defaultValue: userInput, + additionalValidators: [ + (value) { + try { + sourceProvider + .getSource(value ?? '') + .standardizeURL( + preStandardizeUrl(value ?? '')); + } catch (e) { + return e is String + ? e + : e is ObtainiumError + ? e.toString() + : tr('error'); + } + return null; + } + ]) + ] + ], + onValueChanges: (values, valid, isBuilding) { + changeUserInput( + values['appSourceURL']!, valid, isBuilding); + })), + const SizedBox( + width: 16, + ), + gettingAppInfo + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: doingSomething || + pickedSource == null || + (pickedSource!.combinedAppSpecificSettingFormItems + .isNotEmpty && + !additionalSettingsValid) + ? null + : () { + HapticFeedback.selectionClick(); + addApp(); + }, + child: Text(tr('add'))) + ], + ); + + runSearch() async { + setState(() { + searching = true; + }); + try { + var results = await Future.wait(sourceProvider.sources + .where((e) => e.canSearch) + .map((e) => e.search(searchQuery))); + + // .then((results) async { + // Interleave results instead of simple reduce + Map res = {}; + var si = 0; + var done = false; + while (!done) { + done = true; + for (var r in results) { + if (r.length > si) { + done = false; + res.addEntries([r.entries.elementAt(si)]); + } + } + si++; + } + List? selectedUrls = res.isEmpty + ? [] + // ignore: use_build_context_synchronously + : await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return UrlSelectionModal( + urlsWithDescriptions: res, + selectedByDefault: false, + onlyOneSelectionAllowed: true, + ); + }); + if (selectedUrls != null && selectedUrls.isNotEmpty) { + changeUserInput(selectedUrls[0], true, false, isSearch: true); + } + } catch (e) { + showError(e, context); + } finally { + setState(() { + searching = false; + }); + } + } + + bool shouldShowSearchBar() => + sourceProvider.sources.where((e) => e.canSearch).isNotEmpty && + pickedSource == null && + userInput.isEmpty; + + Widget getSearchBarRow() => Row( + children: [ + Expanded( + child: GeneratedForm( + items: [ + [ + GeneratedFormTextField('searchSomeSources', + label: tr('searchSomeSourcesLabel'), required: false), + ] + ], + onValueChanges: (values, valid, isBuilding) { + if (values.isNotEmpty && valid && !isBuilding) { + setState(() { + searchQuery = values['searchSomeSources']!.trim(); + }); + } + }), + ), + const SizedBox( + width: 16, + ), + ElevatedButton( + onPressed: searchQuery.isEmpty || doingSomething + ? null + : () { + runSearch(); + }, + child: Text(tr('search'))) + ], + ); + + Widget getAdditionalOptsCol() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider( + height: 64, + ), + Text( + tr('additionalOptsFor', + args: [pickedSource?.name ?? tr('source')]), + style: TextStyle(color: Theme.of(context).colorScheme.primary)), + const SizedBox( + height: 16, + ), + GeneratedForm( + key: Key(pickedSource.runtimeType.toString()), + items: pickedSource!.combinedAppSpecificSettingFormItems, + onValueChanges: (values, valid, isBuilding) { + if (!isBuilding) { + setState(() { + additionalSettings = values; + additionalSettingsValid = valid; + }); + } + }), + Column( + children: [ + const SizedBox( + height: 16, + ), + CategoryEditorSelector( + alignment: WrapAlignment.start, + onSelected: (categories) { + pickedCategories = categories; + }), + ], + ), + ], + ); + + Widget getSourcesListWidget() => Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 48, + ), + Text( + tr('supportedSourcesBelow'), + ), + const SizedBox( + height: 8, + ), + ...sourceProvider.sources + .map((e) => GestureDetector( + onTap: e.host != null + ? () { + launchUrlString('https://${e.host}', + mode: LaunchMode.externalApplication); + } + : null, + child: Text( + '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', + style: TextStyle( + decoration: e.host != null + ? TextDecoration.underline + : TextDecoration.none, + fontStyle: FontStyle.italic), + ))) + .toList() + ])); + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView(slivers: [ @@ -181,230 +377,16 @@ class _AddAppPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - Expanded( - child: GeneratedForm( - key: Key(searchnum.toString()), - items: [ - [ - GeneratedFormTextField('appSourceURL', - label: tr('appSourceURL'), - defaultValue: userInput, - additionalValidators: [ - (value) { - try { - sourceProvider - .getSource(value ?? '') - .standardizeURL( - preStandardizeUrl( - value ?? '')); - } catch (e) { - return e is String - ? e - : e is ObtainiumError - ? e.toString() - : tr('error'); - } - return null; - } - ]) - ] - ], - onValueChanges: (values, valid, isBuilding) { - changeUserInput(values['appSourceURL']!, - valid, isBuilding); - })), - const SizedBox( - width: 16, - ), - gettingAppInfo - ? const CircularProgressIndicator() - : ElevatedButton( - onPressed: doingSomething || - pickedSource == null || - (pickedSource! - .combinedAppSpecificSettingFormItems - .isNotEmpty && - !additionalSettingsValid) - ? null - : addApp, - child: Text(tr('add'))) - ], - ), - if (sourceProvider.sources - .where((e) => e.canSearch) - .isNotEmpty && - pickedSource == null && - userInput.isEmpty) + getUrlInputRow(), + if (shouldShowSearchBar()) const SizedBox( height: 16, ), - if (sourceProvider.sources - .where((e) => e.canSearch) - .isNotEmpty && - pickedSource == null && - userInput.isEmpty) - Row( - children: [ - Expanded( - child: GeneratedForm( - items: [ - [ - GeneratedFormTextField( - 'searchSomeSources', - label: tr('searchSomeSourcesLabel'), - required: false), - ] - ], - onValueChanges: (values, valid, isBuilding) { - if (values.isNotEmpty && - valid && - !isBuilding) { - setState(() { - searchQuery = - values['searchSomeSources']!.trim(); - }); - } - }), - ), - const SizedBox( - width: 16, - ), - ElevatedButton( - onPressed: searchQuery.isEmpty || doingSomething - ? null - : () { - setState(() { - searching = true; - }); - Future.wait(sourceProvider.sources - .where((e) => e.canSearch) - .map((e) => - e.search(searchQuery))) - .then((results) async { - // Interleave results instead of simple reduce - Map res = {}; - var si = 0; - var done = false; - while (!done) { - done = true; - for (var r in results) { - if (r.length > si) { - done = false; - res.addEntries( - [r.entries.elementAt(si)]); - } - } - si++; - } - List? selectedUrls = res - .isEmpty - ? [] - : await showDialog?>( - context: context, - builder: (BuildContext ctx) { - return UrlSelectionModal( - urlsWithDescriptions: res, - selectedByDefault: false, - onlyOneSelectionAllowed: - true, - ); - }); - if (selectedUrls != null && - selectedUrls.isNotEmpty) { - changeUserInput( - selectedUrls[0], true, false, - isSearch: true); - } - }).catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - searching = false; - }); - }); - }, - child: Text(tr('search'))) - ], - ), + if (shouldShowSearchBar()) getSearchBarRow(), if (pickedSource != null) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Divider( - height: 64, - ), - Text( - tr('additionalOptsFor', - args: [pickedSource?.name ?? tr('source')]), - style: TextStyle( - color: - Theme.of(context).colorScheme.primary)), - const SizedBox( - height: 16, - ), - GeneratedForm( - key: Key(pickedSource.runtimeType.toString()), - items: pickedSource! - .combinedAppSpecificSettingFormItems, - onValueChanges: (values, valid, isBuilding) { - if (!isBuilding) { - setState(() { - additionalSettings = values; - additionalSettingsValid = valid; - }); - } - }), - Column( - children: [ - const SizedBox( - height: 16, - ), - CategoryEditorSelector( - alignment: WrapAlignment.start, - onSelected: (categories) { - pickedCategories = categories; - }), - ], - ), - ], - ) + getAdditionalOptsCol() else - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 48, - ), - Text( - tr('supportedSourcesBelow'), - ), - const SizedBox( - height: 8, - ), - ...sourceProvider.sources - .map((e) => GestureDetector( - onTap: e.host != null - ? () { - launchUrlString( - 'https://${e.host}', - mode: LaunchMode - .externalApplication); - } - : null, - child: Text( - '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', - style: TextStyle( - decoration: e.host != null - ? TextDecoration.underline - : TextDecoration.none, - fontStyle: FontStyle.italic), - ))) - .toList() - ])), + getSourcesListWidget(), const SizedBox( height: 8, ), diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 4a73b72..701cb56 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/main.dart'; @@ -34,406 +35,414 @@ class _AppPageState extends State { }); } + bool areDownloadsRunning = appsProvider.areDownloadsRunning(); + var sourceProvider = SourceProvider(); AppInMemory? app = appsProvider.apps[widget.appId]; var source = app != null ? sourceProvider.getSource(app.app.url) : null; - if (!appsProvider.areDownloadsRunning() && prevApp == null && app != null) { + if (!areDownloadsRunning && prevApp == null && app != null) { prevApp = app; getUpdate(app.app.id); } var trackOnly = app?.app.additionalSettings['trackOnly'] == true; - var infoColumn = Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GestureDetector( - onTap: () { - if (app?.app.url != null) { - launchUrlString(app?.app.url ?? '', - mode: LaunchMode.externalApplication); - } - }, - child: Text( - app?.app.url ?? '', - textAlign: TextAlign.center, - style: const TextStyle( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic, - fontSize: 12), - )), - const SizedBox( - height: 32, - ), - Text( - tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - '${tr('installedVersionX', args: [ - app?.app.installedVersion ?? tr('none') - ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ - tr('app') - ])}' : ''}', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox( - height: 32, - ), - Text( - tr('lastUpdateCheckX', args: [ - app?.app.lastUpdateCheck == null - ? tr('never') - : '\n${app?.app.lastUpdateCheck?.toLocal()}' - ]), - textAlign: TextAlign.center, - style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), - ), - const SizedBox( - height: 48, - ), - CategoryEditorSelector( - alignment: WrapAlignment.center, - preselected: - app?.app.categories != null ? app!.app.categories.toSet() : {}, - onSelected: (categories) { - if (app != null) { - app.app.categories = categories; - appsProvider.saveApps([app.app]); - } - }), - ], - ); + bool isVersionDetectionStandard = + app?.app.additionalSettings['versionDetection'] == + 'standardVersionDetection'; - var fullInfoColumn = Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 125), - app?.installedInfo != null - ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.memory( - app!.installedInfo!.icon!, - height: 150, - gaplessPlayback: true, - ) - ]) - : Container(), - const SizedBox( - height: 25, - ), - Text( - app?.installedInfo?.name ?? app?.app.name ?? tr('app'), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.displayLarge, - ), - Text( - tr('byX', args: [app?.app.author ?? tr('unknown')]), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox( - height: 8, - ), - Text( - app?.app.id ?? '', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall, - ), - app?.app.releaseDate == null - ? const SizedBox.shrink() - : Text( - app!.app.releaseDate.toString(), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelSmall, + getInfoColumn() => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () { + if (app?.app.url != null) { + launchUrlString(app?.app.url ?? '', + mode: LaunchMode.externalApplication); + } + }, + child: Text( + app?.app.url ?? '', + textAlign: TextAlign.center, + style: const TextStyle( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, + fontSize: 12), + )), + const SizedBox( + height: 32, + ), + Text( + tr('latestVersionX', + args: [app?.app.latestVersion ?? tr('unknown')]), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + '${tr('installedVersionX', args: [ + app?.installedInfo?.versionName ?? + app?.app.installedVersion ?? + tr('none') + ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ + tr('app') + ])}' : ''}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (app?.app.installedVersion != null && + !isVersionDetectionStandard) + Column( + children: [ + const SizedBox( + height: 4, + ), + Text( + tr('noVersionDetection'), + style: Theme.of(context).textTheme.labelSmall, + ) + ], ), - const SizedBox( - height: 32, - ), - infoColumn, - const SizedBox(height: 150) - ], - ); + const SizedBox( + height: 32, + ), + Text( + tr('lastUpdateCheckX', args: [ + app?.app.lastUpdateCheck == null + ? tr('never') + : '\n${app?.app.lastUpdateCheck?.toLocal()}' + ]), + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), + ), + const SizedBox( + height: 48, + ), + CategoryEditorSelector( + alignment: WrapAlignment.center, + preselected: app?.app.categories != null + ? app!.app.categories.toSet() + : {}, + onSelected: (categories) { + if (app != null) { + app.app.categories = categories; + appsProvider.saveApps([app.app]); + } + }), + ], + ); + + getFullInfoColumn() => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 125), + app?.installedInfo != null + ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Image.memory( + app!.installedInfo!.icon!, + height: 150, + gaplessPlayback: true, + ) + ]) + : Container(), + const SizedBox( + height: 25, + ), + Text( + app?.installedInfo?.name ?? app?.app.name ?? tr('app'), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.displayLarge, + ), + Text( + tr('byX', args: [app?.app.author ?? tr('unknown')]), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox( + height: 8, + ), + Text( + app?.app.id ?? '', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall, + ), + app?.app.releaseDate == null + ? const SizedBox.shrink() + : Text( + app!.app.releaseDate.toString(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox( + height: 32, + ), + getInfoColumn(), + const SizedBox(height: 150) + ], + ); + + getAppWebView() => app != null + ? WebViewWidget( + controller: WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Theme.of(context).colorScheme.background) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onWebResourceError: (WebResourceError error) { + if (error.isForMainFrame == true) { + showError( + ObtainiumError(error.description, unexpected: true), + context); + } + }, + ), + ) + ..loadRequest(Uri.parse(app.app.url))) + : Container(); + + showMarkUpdatedDialog() { + return showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text(tr('alreadyUpToDateQuestion')), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('no'))), + TextButton( + onPressed: () { + HapticFeedback.selectionClick(); + var updatedApp = app?.app; + if (updatedApp != null) { + updatedApp.installedVersion = updatedApp.latestVersion; + appsProvider.saveApps([updatedApp]); + } + Navigator.of(context).pop(); + }, + child: Text(tr('yesMarkUpdated'))) + ], + ); + }); + } + + showAdditionalOptionsDialog() async { + return await showDialog?>( + context: context, + builder: (BuildContext ctx) { + var items = + (source?.combinedAppSpecificSettingFormItems ?? []).map((row) { + row = row.map((e) { + if (app?.app.additionalSettings[e.key] != null) { + e.defaultValue = app?.app.additionalSettings[e.key]; + } + return e; + }).toList(); + return row; + }).toList(); + + items = items.map((row) { + row = row.map((e) { + if (e.key == 'versionDetection' && e is GeneratedFormDropdown) { + e.disabledOptKeys ??= []; + if (app?.app.installedVersion != null && + !appsProvider.isVersionDetectionPossible(app)) { + e.disabledOptKeys!.add('standardVersionDetection'); + } + if (app?.app.releaseDate == null) { + e.disabledOptKeys!.add('releaseDateAsVersion'); + } + } + return e; + }).toList(); + return row; + }).toList(); + + return GeneratedFormModal( + title: tr('additionalOptions'), + items: items, + ); + }); + } + + handleAdditionalOptionChanges(Map? values) { + if (app != null && values != null) { + Map originalSettings = app.app.additionalSettings; + app.app.additionalSettings = values; + if (source?.enforceTrackOnly == true) { + app.app.additionalSettings['trackOnly'] = true; + // ignore: use_build_context_synchronously + showError(tr('appsFromSourceAreTrackOnly'), context); + } + if (app.app.additionalSettings['versionDetection'] == + 'releaseDateAsVersion') { + if (originalSettings['versionDetection'] != 'releaseDateAsVersion') { + if (app.app.releaseDate != null) { + bool isUpdated = + app.app.installedVersion == app.app.latestVersion; + app.app.latestVersion = + app.app.releaseDate!.microsecondsSinceEpoch.toString(); + if (isUpdated) { + app.app.installedVersion = app.app.latestVersion; + } + } + } + } else if (originalSettings['versionDetection'] == + 'releaseDateAsVersion') { + app.app.installedVersion = + app.installedInfo?.versionName ?? app.app.installedVersion; + } + appsProvider.saveApps([app.app]).then((value) { + getUpdate(app.app.id); + }); + } + } + + getInstallOrUpdateButton() => TextButton( + onPressed: (app?.app.installedVersion == null || + app?.app.installedVersion != app?.app.latestVersion) && + !areDownloadsRunning + ? () async { + try { + HapticFeedback.heavyImpact(); + if (app?.app.additionalSettings['trackOnly'] != true) { + await settingsProvider.getInstallPermission(); + } + var res = await appsProvider.downloadAndInstallLatestApps( + [app!.app.id], globalNavigatorKey.currentContext); + if (res.isNotEmpty && mounted) { + Navigator.of(context).pop(); + } + } catch (e) { + showError(e, context); + } + } + : null, + child: Text(app?.app.installedVersion == null + ? !trackOnly + ? tr('install') + : tr('markInstalled') + : !trackOnly + ? tr('update') + : tr('markUpdated'))); + + getBottomSheetMenu() => Padding( + padding: + EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (app?.app.installedVersion != null && + app?.app.installedVersion != app?.app.latestVersion && + !isVersionDetectionStandard && + !trackOnly) + IconButton( + onPressed: app?.downloadProgress != null + ? null + : showMarkUpdatedDialog, + tooltip: tr('markUpdated'), + icon: const Icon(Icons.done)), + if (source != null && + source.combinedAppSpecificSettingFormItems.isNotEmpty) + IconButton( + onPressed: app?.downloadProgress != null + ? null + : () async { + var values = + await showAdditionalOptionsDialog(); + handleAdditionalOptionChanges(values); + }, + tooltip: tr('additionalOptions'), + icon: const Icon(Icons.edit)), + if (app != null && app.installedInfo != null) + IconButton( + onPressed: () { + appsProvider.openAppSettings(app.app.id); + }, + icon: const Icon(Icons.settings), + tooltip: tr('settings'), + ), + if (app != null && settingsProvider.showAppWebpage) + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + content: getInfoColumn(), + title: Text( + '${app.app.name} ${tr('byX', args: [ + app.app.author + ])}'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('continue'))) + ], + ); + }); + }, + icon: const Icon(Icons.more_horiz), + tooltip: tr('more')), + const SizedBox(width: 16.0), + Expanded(child: getInstallOrUpdateButton()), + const SizedBox(width: 16.0), + Expanded( + child: TextButton( + onPressed: app?.downloadProgress != null + ? null + : () { + appsProvider.removeAppsWithModal( + context, [app!.app]).then((value) { + if (value == true) { + Navigator.of(context).pop(); + } + }); + }, + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.error, + surfaceTintColor: + Theme.of(context).colorScheme.error), + child: Text(tr('remove')), + )), + ])), + if (app?.downloadProgress != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), + child: LinearProgressIndicator( + value: app!.downloadProgress! / 100)) + ], + )); return Scaffold( - appBar: settingsProvider.showAppWebpage ? AppBar() : null, - backgroundColor: Theme.of(context).colorScheme.surface, - body: RefreshIndicator( - child: settingsProvider.showAppWebpage - ? app != null - ? WebViewWidget( - controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor( - Theme.of(context).colorScheme.background) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - onWebResourceError: (WebResourceError error) { - if (error.isForMainFrame == true) { - showError( - ObtainiumError(error.description, - unexpected: true), - context); - } - }, - ), - ) - ..loadRequest(Uri.parse(app.app.url))) - : Container() - : CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Column(children: [fullInfoColumn])), - ], - ), - onRefresh: () async { - if (app != null) { - getUpdate(app.app.id); - } - }), - bottomSheet: Padding( - padding: EdgeInsets.fromLTRB( - 0, 0, 0, MediaQuery.of(context).padding.bottom), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (app?.app.additionalSettings['versionDetection'] != - 'standardVersionDetection' && - !trackOnly && - app?.app.installedVersion != null && - app?.app.installedVersion != app?.app.latestVersion) - IconButton( - onPressed: app?.downloadProgress != null - ? null - : () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - title: Text(tr( - 'alreadyUpToDateQuestion')), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context) - .pop(); - }, - child: Text(tr('no'))), - TextButton( - onPressed: () { - HapticFeedback - .selectionClick(); - var updatedApp = app?.app; - if (updatedApp != null) { - updatedApp - .installedVersion = - updatedApp - .latestVersion; - appsProvider.saveApps( - [updatedApp]); - } - Navigator.of(context) - .pop(); - }, - child: Text( - tr('yesMarkUpdated'))) - ], - ); - }); - }, - tooltip: tr('markUpdated'), - icon: const Icon(Icons.done)), - if (source != null && - source - .combinedAppSpecificSettingFormItems.isNotEmpty) - IconButton( - onPressed: app?.downloadProgress != null - ? null - : () { - showDialog?>( - context: context, - 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( - title: tr('additionalOptions'), - items: items, - ); - }).then((values) { - if (app != null && values != null) { - Map - originalSettings = - app.app.additionalSettings; - app.app.additionalSettings = values; - if (source.enforceTrackOnly) { - app.app.additionalSettings[ - 'trackOnly'] = true; - showError( - tr('appsFromSourceAreTrackOnly'), - context); - } - if (app.app.additionalSettings[ - 'versionDetection'] == - 'releaseDateAsVersion') { - if (originalSettings[ - 'versionDetection'] != - 'releaseDateAsVersion') { - if (app.app.releaseDate != null) { - bool isUpdated = - app.app.installedVersion == - app.app.latestVersion; - app.app.latestVersion = app - .app - .releaseDate! - .microsecondsSinceEpoch - .toString(); - if (isUpdated) { - app.app.installedVersion = - app.app.latestVersion; - } - } - } - } else if (originalSettings[ - 'versionDetection'] == - 'releaseDateAsVersion') { - app.app.installedVersion = app - .installedInfo - ?.versionName ?? - app.app.installedVersion; - } - appsProvider.saveApps([app.app]).then( - (value) { - getUpdate(app.app.id); - }); - } - }); - }, - tooltip: tr('additionalOptions'), - icon: const Icon(Icons.edit)), - if (app != null && app.installedInfo != null) - IconButton( - onPressed: () { - appsProvider.openAppSettings(app.app.id); - }, - icon: const Icon(Icons.settings), - tooltip: tr('settings'), - ), - if (app != null && settingsProvider.showAppWebpage) - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - content: infoColumn, - title: Text( - '${app.app.name} ${tr('byX', args: [ - app.app.author - ])}'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('continue'))) - ], - ); - }); - }, - icon: const Icon(Icons.more_horiz), - tooltip: tr('more')), - const SizedBox(width: 16.0), - Expanded( - child: TextButton( - onPressed: (app?.app.installedVersion == null || - app?.app.installedVersion != - app?.app.latestVersion) && - !appsProvider.areDownloadsRunning() - ? () { - HapticFeedback.heavyImpact(); - () async { - if (app?.app.additionalSettings[ - 'trackOnly'] != - true) { - await settingsProvider - .getInstallPermission(); - } - }() - .then((value) { - appsProvider - .downloadAndInstallLatestApps( - [app!.app.id], - globalNavigatorKey - .currentContext).then( - (res) { - if (res.isNotEmpty && mounted) { - Navigator.of(context).pop(); - } - }).catchError((e) { - showError(e, context); - }); - }).catchError((e) { - showError(e, context); - }); - } - : null, - child: Text(app?.app.installedVersion == null - ? !trackOnly - ? tr('install') - : tr('markInstalled') - : !trackOnly - ? tr('update') - : tr('markUpdated')))), - const SizedBox(width: 16.0), - Expanded( - child: TextButton( - onPressed: app?.downloadProgress != null - ? null - : () { - appsProvider.removeAppsWithModal( - context, [app!.app]).then((value) { - if (value == true) { - Navigator.of(context).pop(); - } - }); - }, - style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.error, - surfaceTintColor: - Theme.of(context).colorScheme.error), - child: Text(tr('remove')), - )), - ])), - if (app?.downloadProgress != null) - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), - child: LinearProgressIndicator( - value: app!.downloadProgress! / 100)) - ], - )), - ); + appBar: settingsProvider.showAppWebpage ? AppBar() : null, + backgroundColor: Theme.of(context).colorScheme.surface, + body: RefreshIndicator( + child: settingsProvider.showAppWebpage + ? getAppWebView() + : CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column(children: [getFullInfoColumn()])), + ], + ), + onRefresh: () async { + if (app != null) { + getUpdate(app.app.id); + } + }), + bottomSheet: getBottomSheetMenu()); } } diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index a0beb61..d8b59f6 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -187,6 +187,722 @@ class AppsPageState extends State { } listedApps = [...tempPinned, ...tempNotPinned]; + showChangeLogDialog( + String? changesUrl, AppSource appSource, String changeLog, int index) { + showDialog( + context: context, + builder: (BuildContext context) { + return GeneratedFormModal( + title: tr('changes'), + items: const [], + additionalWidgets: [ + changesUrl != null + ? GestureDetector( + child: Text( + changesUrl, + style: const TextStyle( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic), + ), + onTap: () { + launchUrlString(changesUrl, + mode: LaunchMode.externalApplication); + }, + ) + : const SizedBox.shrink(), + changesUrl != null + ? const SizedBox( + height: 16, + ) + : const SizedBox.shrink(), + appSource.changeLogIfAnyIsMarkDown + ? SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 350, + child: Markdown( + data: changeLog, + onTapLink: (text, href, title) { + if (href != null) { + launchUrlString( + href.startsWith('http://') || + href.startsWith('https://') + ? href + : '${Uri.parse(listedApps[index].app.url).origin}/$href', + mode: LaunchMode.externalApplication); + } + }, + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [ + md.EmojiSyntax(), + ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes + ], + ), + )) + : Text(changeLog), + ], + singleNullReturnButton: tr('ok'), + ); + }); + } + + getLoadingWidgets() { + return [ + if (appsProvider.loadingApps || listedApps.isEmpty) + SliverFillRemaining( + child: Center( + child: appsProvider.loadingApps + ? const CircularProgressIndicator() + : Text( + appsProvider.apps.isEmpty + ? tr('noApps') + : tr('noAppsForFilter'), + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ))), + if (refreshingSince != null) + SliverToBoxAdapter( + child: LinearProgressIndicator( + value: appsProvider.apps.values + .where((element) => !(element.app.lastUpdateCheck + ?.isBefore(refreshingSince!) ?? + true)) + .length / + appsProvider.apps.length, + ), + ) + ]; + } + + getChangeLogFn(int appIndex) { + AppSource appSource = + SourceProvider().getSource(listedApps[appIndex].app.url); + String? changesUrl = + appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url); + String? changeLog = listedApps[appIndex].app.changeLog; + return (changeLog == null && changesUrl == null) + ? null + : () { + if (changeLog != null) { + showChangeLogDialog(changesUrl, appSource, changeLog, appIndex); + } else { + launchUrlString(changesUrl!, + mode: LaunchMode.externalApplication); + } + }; + } + + getUpdateButton(int appIndex) { + return IconButton( + visualDensity: VisualDensity.compact, + color: Theme.of(context).colorScheme.primary, + tooltip: + listedApps[appIndex].app.additionalSettings['trackOnly'] == true + ? tr('markUpdated') + : tr('update'), + onPressed: appsProvider.areDownloadsRunning() + ? null + : () { + appsProvider.downloadAndInstallLatestApps( + [listedApps[appIndex].app.id], + globalNavigatorKey.currentContext).catchError((e) { + showError(e, context); + }); + }, + icon: Icon( + listedApps[appIndex].app.additionalSettings['trackOnly'] == true + ? Icons.check_circle_outline + : Icons.install_mobile)); + } + + getAppIcon(int appIndex) { + return listedApps[appIndex].installedInfo != null + ? Image.memory( + listedApps[appIndex].installedInfo!.icon!, + gaplessPlayback: true, + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform( + alignment: Alignment.center, + transform: Matrix4.rotationZ(0.31), + child: Padding( + padding: const EdgeInsets.all(15), + child: Image( + image: const AssetImage( + 'assets/graphics/icon_small.png'), + color: Colors.white.withOpacity(0.1), + colorBlendMode: BlendMode.modulate, + gaplessPlayback: true, + ), + )), + ]); + } + + getVersionText(int appIndex) { + return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}'; + } + + getChangesButtonString(int appIndex, bool hasChangeLogFn) { + return listedApps[appIndex].app.releaseDate == null + ? hasChangeLogFn + ? tr('changes') + : '' + : DateFormat('yyyy-MM-dd') + .format(listedApps[appIndex].app.releaseDate!); + } + + getSingleAppHorizTile(int index) { + var showChangesFn = getChangeLogFn(index); + var hasUpdate = listedApps[index].app.installedVersion != null && + listedApps[index].app.installedVersion != + listedApps[index].app.latestVersion; + Widget trailingRow = Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + hasUpdate ? getUpdateButton(index) : const SizedBox.shrink(), + hasUpdate + ? const SizedBox( + width: 10, + ) + : const SizedBox.shrink(), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row(mainAxisSize: MainAxisSize.min, children: [ + Container( + constraints: const BoxConstraints(maxWidth: 150), + child: Text( + getVersionText(index), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, + )), + ]), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: showChangesFn, + child: Text( + getChangesButtonString(index, showChangesFn != null), + style: TextStyle( + fontStyle: FontStyle.italic, + decoration: showChangesFn != null + ? TextDecoration.underline + : TextDecoration.none), + )) + ], + ), + ], + ) + ], + ); + + var transparent = const Color.fromARGB(0, 0, 0, 0).value; + return Container( + decoration: BoxDecoration( + border: Border.symmetric( + vertical: BorderSide( + width: 4, + color: Color(listedApps[index].app.categories.isNotEmpty + ? settingsProvider.categories[ + listedApps[index].app.categories.first] ?? + transparent + : transparent)))), + child: ListTile( + tileColor: listedApps[index].app.pinned + ? Colors.grey.withOpacity(0.1) + : Colors.transparent, + selectedTileColor: Theme.of(context) + .colorScheme + .primary + .withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1), + selected: selectedApps.contains(listedApps[index].app), + onLongPress: () { + toggleAppSelected(listedApps[index].app); + }, + leading: getAppIcon(index), + title: Text( + maxLines: 1, + listedApps[index].installedInfo?.name ?? + listedApps[index].app.name, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal, + ), + ), + subtitle: Text(tr('byX', args: [listedApps[index].app.author]), + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + fontWeight: listedApps[index].app.pinned + ? FontWeight.bold + : FontWeight.normal)), + trailing: listedApps[index].downloadProgress != null + ? Text(tr('percentProgress', args: [ + listedApps[index].downloadProgress?.toInt().toString() ?? + '100' + ])) + : trailingRow, + onTap: () { + if (selectedApps.isNotEmpty) { + toggleAppSelected(listedApps[index].app); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppPage(appId: listedApps[index].app.id)), + ); + } + }, + )); + } + + getSelectAllButton() { + return selectedApps.isEmpty + ? TextButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), + onPressed: () { + selectThese(listedApps.map((e) => e.app).toList()); + }, + icon: Icon( + Icons.select_all_outlined, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(listedApps.length.toString())) + : TextButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), + onPressed: () { + selectedApps.isEmpty + ? selectThese(listedApps.map((e) => e.app).toList()) + : clearSelected(); + }, + icon: Icon( + selectedApps.isEmpty + ? Icons.select_all_outlined + : Icons.deselect_outlined, + color: Theme.of(context).colorScheme.primary, + ), + label: Text(selectedApps.length.toString())); + } + + getMassObtainFunction() { + return appsProvider.areDownloadsRunning() || + (existingUpdateIdsAllOrSelected.isEmpty && + newInstallIdsAllOrSelected.isEmpty && + trackOnlyUpdateIdsAllOrSelected.isEmpty) + ? null + : () { + HapticFeedback.heavyImpact(); + List formItems = []; + if (existingUpdateIdsAllOrSelected.isNotEmpty) { + formItems.add(GeneratedFormSwitch('updates', + label: tr('updateX', args: [ + plural('apps', existingUpdateIdsAllOrSelected.length) + ]), + defaultValue: true)); + } + if (newInstallIdsAllOrSelected.isNotEmpty) { + formItems.add(GeneratedFormSwitch('installs', + label: tr('installX', args: [ + plural('apps', newInstallIdsAllOrSelected.length) + ]), + defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty)); + } + if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { + formItems.add(GeneratedFormSwitch('trackonlies', + label: tr('markXTrackOnlyAsUpdated', args: [ + plural('apps', trackOnlyUpdateIdsAllOrSelected.length) + ]), + defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty || + newInstallIdsAllOrSelected.isNotEmpty)); + } + showDialog?>( + context: context, + builder: (BuildContext ctx) { + var totalApps = existingUpdateIdsAllOrSelected.length + + newInstallIdsAllOrSelected.length + + trackOnlyUpdateIdsAllOrSelected.length; + return GeneratedFormModal( + title: tr('changeX', args: [plural('apps', totalApps)]), + items: formItems.map((e) => [e]).toList(), + initValid: true, + ); + }).then((values) { + if (values != null) { + if (values.isEmpty) { + values = getDefaultValuesFromFormItems([formItems]); + } + bool shouldInstallUpdates = values['updates'] == true; + bool shouldInstallNew = values['installs'] == true; + bool shouldMarkTrackOnlies = values['trackonlies'] == true; + (() async { + if (shouldInstallNew || shouldInstallUpdates) { + await settingsProvider.getInstallPermission(); + } + })() + .then((_) { + List toInstall = []; + if (shouldInstallUpdates) { + toInstall.addAll(existingUpdateIdsAllOrSelected); + } + if (shouldInstallNew) { + toInstall.addAll(newInstallIdsAllOrSelected); + } + if (shouldMarkTrackOnlies) { + toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); + } + appsProvider + .downloadAndInstallLatestApps( + toInstall, globalNavigatorKey.currentContext) + .catchError((e) { + showError(e, context); + }); + }); + } + }); + }; + } + + launchCategorizeDialog() { + return () async { + try { + Set? preselected; + var showPrompt = false; + for (var element in selectedApps) { + var currentCats = element.categories.toSet(); + if (preselected == null) { + preselected = currentCats; + } else { + if (!settingsProvider.setEqual(currentCats, preselected)) { + showPrompt = true; + break; + } + } + } + var cont = true; + if (showPrompt) { + cont = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('categorize'), + items: const [], + initValid: true, + message: tr('selectedCategorizeWarning'), + ); + }) != + null; + } + if (cont) { + // ignore: use_build_context_synchronously + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('categorize'), + items: const [], + initValid: true, + singleNullReturnButton: tr('continue'), + additionalWidgets: [ + CategoryEditorSelector( + preselected: !showPrompt ? preselected ?? {} : {}, + showLabelWhenNotEmpty: false, + onSelected: (categories) { + appsProvider.saveApps(selectedApps.map((e) { + e.categories = categories; + return e; + }).toList()); + }, + ) + ], + ); + }); + } + } catch (err) { + showError(err, context); + } + }; + } + + showMassMarkDialog() { + return showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: Text(tr('markXSelectedAppsAsUpdated', + args: [selectedApps.length.toString()])), + content: Text( + tr('onlyWorksWithNonVersionDetectApps'), + style: const TextStyle( + fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(tr('no'))), + TextButton( + onPressed: () { + HapticFeedback.selectionClick(); + appsProvider.saveApps(selectedApps.map((a) { + if (a.installedVersion != null && + !appsProvider.isVersionDetectionPossible( + appsProvider.apps[a.id])) { + a.installedVersion = a.latestVersion; + } + return a; + }).toList()); + + Navigator.of(context).pop(); + }, + child: Text(tr('yes'))) + ], + ); + }).whenComplete(() { + Navigator.of(context).pop(); + }); + } + + pinSelectedApps() { + () { + var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; + appsProvider.saveApps(selectedApps.map((e) { + e.pinned = pinStatus; + return e; + }).toList()); + Navigator.of(context).pop(); + }; + } + + resetSelectedAppsInstallStatuses() { + () async { + try { + var values = await showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('resetInstallStatusForSelectedAppsQuestion'), + items: const [], + initValid: true, + message: tr('installStatusOfXWillBeResetExplanation', + args: [plural('app', selectedApps.length)]), + ); + }); + if (values != null) { + appsProvider.saveApps(selectedApps.map((e) { + e.installedVersion = null; + return e; + }).toList()); + } + } finally { + Navigator.of(context).pop(); + } + }; + } + + showMoreOptionsDialog() { + return showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + scrollable: true, + content: Padding( + padding: const EdgeInsets.only(top: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + 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: tr('selectedAppURLsFromObtainium')); + Navigator.of(context).pop(); + }, + tooltip: tr('shareSelectedAppURLs'), + icon: const Icon(Icons.share), + ), + IconButton( + onPressed: resetSelectedAppsInstallStatuses(), + tooltip: tr('resetInstallStatus'), + icon: const Icon(Icons.restore_page_outlined), + ), + ]), + ), + ); + }); + } + + getMainBottomButtonsRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + visualDensity: VisualDensity.compact, + onPressed: selectedApps.isEmpty + ? null + : () { + appsProvider.removeAppsWithModal( + context, selectedApps.toList()); + }, + tooltip: tr('removeSelectedApps'), + icon: const Icon(Icons.delete_outline_outlined), + ), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: getMassObtainFunction(), + tooltip: selectedApps.isEmpty + ? tr('installUpdateApps') + : tr('installUpdateSelectedApps'), + icon: const Icon( + Icons.file_download_outlined, + )), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: selectedApps.isEmpty ? null : launchCategorizeDialog(), + tooltip: tr('categorize'), + icon: const Icon(Icons.category_outlined), + ), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: selectedApps.isEmpty ? null : showMoreOptionsDialog, + tooltip: tr('more'), + icon: const Icon(Icons.more_horiz), + ), + ], + ); + } + + showFilterDialog() async { + var values = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + var vals = filter.toFormValuesMap(); + return GeneratedFormModal( + initValid: true, + title: tr('filterApps'), + items: [ + [ + GeneratedFormTextField('appName', + label: tr('appName'), + required: false, + defaultValue: vals['appName']), + GeneratedFormTextField('author', + label: tr('author'), + required: false, + defaultValue: vals['author']) + ], + [ + GeneratedFormSwitch('upToDateApps', + label: tr('upToDateApps'), + defaultValue: vals['upToDateApps']) + ], + [ + GeneratedFormSwitch('nonInstalledApps', + label: tr('nonInstalledApps'), + defaultValue: vals['nonInstalledApps']) + ] + ], + additionalWidgets: [ + const SizedBox( + height: 16, + ), + CategoryEditorSelector( + preselected: filter.categoryFilter, + onSelected: (categories) { + filter.categoryFilter = categories.toSet(); + }, + ) + ], + ); + }); + if (values != null) { + setState(() { + filter.setFormValuesFromMap(values); + }); + } + } + + getFilterButtonsRow() { + return Row( + children: [ + getSelectAllButton(), + const VerticalDivider(), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: getMainBottomButtonsRow())), + const VerticalDivider(), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + setState(() { + if (currentFilterIsUpdatesOnly) { + filter = AppsFilter(); + } else { + filter = updatesOnlyFilter; + } + }); + }, + tooltip: currentFilterIsUpdatesOnly + ? tr('removeOutdatedFilter') + : tr('showOutdatedOnly'), + icon: Icon( + currentFilterIsUpdatesOnly + ? Icons.update_disabled_rounded + : Icons.update_rounded, + color: Theme.of(context).colorScheme.primary, + ), + ), + TextButton.icon( + style: const ButtonStyle(visualDensity: VisualDensity.compact), + label: Text( + filter.isIdenticalTo(neutralFilter, settingsProvider) + ? tr('filter') + : tr('filterActive'), + style: TextStyle( + fontWeight: + filter.isIdenticalTo(neutralFilter, settingsProvider) + ? FontWeight.normal + : FontWeight.bold), + ), + onPressed: showFilterDialog, + icon: const Icon(Icons.filter_list_rounded)) + ], + ); + } + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: RefreshIndicator( @@ -205,823 +921,17 @@ class AppsPageState extends State { }, child: CustomScrollView(slivers: [ CustomAppBar(title: tr('appsString')), - if (appsProvider.loadingApps || listedApps.isEmpty) - SliverFillRemaining( - child: Center( - child: appsProvider.loadingApps - ? const CircularProgressIndicator() - : Text( - appsProvider.apps.isEmpty - ? tr('noApps') - : tr('noAppsForFilter'), - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ))), - if (refreshingSince != null) - SliverToBoxAdapter( - child: LinearProgressIndicator( - value: appsProvider.apps.values - .where((element) => !(element.app.lastUpdateCheck - ?.isBefore(refreshingSince!) ?? - true)) - .length / - appsProvider.apps.length, - ), - ), + ...getLoadingWidgets(), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { - AppSource appSource = - SourceProvider().getSource(listedApps[index].app.url); - String? changesUrl = appSource - .changeLogPageFromStandardUrl(listedApps[index].app.url); - String? changeLog = listedApps[index].app.changeLog; - var showChanges = (changeLog == null && changesUrl == null) - ? null - : () { - if (changeLog != null) { - showDialog( - context: context, - builder: (BuildContext context) { - return GeneratedFormModal( - title: tr('changes'), - items: const [], - additionalWidgets: [ - changesUrl != null - ? GestureDetector( - child: Text( - changesUrl, - style: const TextStyle( - decoration: - TextDecoration.underline, - fontStyle: FontStyle.italic), - ), - onTap: () { - launchUrlString(changesUrl, - mode: LaunchMode - .externalApplication); - }, - ) - : const SizedBox.shrink(), - changesUrl != null - ? const SizedBox( - height: 16, - ) - : const SizedBox.shrink(), - appSource.changeLogIfAnyIsMarkDown - ? SizedBox( - width: - MediaQuery.of(context).size.width, - height: MediaQuery.of(context) - .size - .height - - 350, - child: Markdown( - data: changeLog, - onTapLink: (text, href, title) { - if (href != null) { - launchUrlString( - href.startsWith( - 'http://') || - href.startsWith( - 'https://') - ? href - : '${Uri.parse(listedApps[index].app.url).origin}/$href', - mode: LaunchMode - .externalApplication); - } - }, - extensionSet: md.ExtensionSet( - md.ExtensionSet.gitHubFlavored - .blockSyntaxes, - [ - md.EmojiSyntax(), - ...md - .ExtensionSet - .gitHubFlavored - .inlineSyntaxes - ], - ), - )) - : Text(changeLog), - ], - singleNullReturnButton: tr('ok'), - ); - }); - } else { - launchUrlString(changesUrl!, - mode: LaunchMode.externalApplication); - } - }; - var transparent = const Color.fromARGB(0, 0, 0, 0).value; - var hasUpdate = listedApps[index].app.installedVersion != null && - listedApps[index].app.installedVersion != - listedApps[index].app.latestVersion; - var updateButton = IconButton( - visualDensity: VisualDensity.compact, - color: Theme.of(context).colorScheme.primary, - tooltip: - listedApps[index].app.additionalSettings['trackOnly'] == - true - ? tr('markUpdated') - : tr('update'), - onPressed: appsProvider.areDownloadsRunning() - ? null - : () { - appsProvider.downloadAndInstallLatestApps([ - listedApps[index].app.id - ], globalNavigatorKey.currentContext).catchError((e) { - showError(e, context); - }); - }, - icon: Icon( - listedApps[index].app.additionalSettings['trackOnly'] == - true - ? Icons.check_circle_outline - : Icons.install_mobile)); - return Container( - decoration: BoxDecoration( - border: Border.symmetric( - vertical: BorderSide( - width: 4, - color: Color( - listedApps[index].app.categories.isNotEmpty - ? settingsProvider.categories[ - listedApps[index] - .app - .categories - .first] ?? - transparent - : transparent)))), - child: ListTile( - tileColor: listedApps[index].app.pinned - ? Colors.grey.withOpacity(0.1) - : Colors.transparent, - selectedTileColor: Theme.of(context) - .colorScheme - .primary - .withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1), - selected: selectedApps.contains(listedApps[index].app), - onLongPress: () { - toggleAppSelected(listedApps[index].app); - }, - leading: listedApps[index].installedInfo != null - ? Image.memory( - listedApps[index].installedInfo!.icon!, - gaplessPlayback: true, - ) - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationZ(0.31), - child: Padding( - padding: const EdgeInsets.all(15), - child: Image( - image: const AssetImage( - 'assets/graphics/icon_small.png'), - color: Colors.white.withOpacity(0.1), - colorBlendMode: BlendMode.modulate, - gaplessPlayback: true, - ), - )), - ]), - title: Text( - maxLines: 1, - listedApps[index].installedInfo?.name ?? - listedApps[index].app.name, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontWeight: listedApps[index].app.pinned - ? FontWeight.bold - : FontWeight.normal, - ), - ), - subtitle: Text( - tr('byX', args: [listedApps[index].app.author]), - maxLines: 1, - style: TextStyle( - overflow: TextOverflow.ellipsis, - fontWeight: listedApps[index].app.pinned - ? FontWeight.bold - : FontWeight.normal)), - trailing: listedApps[index].downloadProgress != null - ? Text(tr('percentProgress', args: [ - listedApps[index] - .downloadProgress - ?.toInt() - .toString() ?? - '100' - ])) - : (Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - hasUpdate - ? updateButton - : const SizedBox.shrink(), - hasUpdate - ? const SizedBox( - width: 10, - ) - : const SizedBox.shrink(), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - constraints: const BoxConstraints( - maxWidth: 150), - child: Text( - '${listedApps[index].app.installedVersion ?? tr('notInstalled')}${listedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}', - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - )), - ]), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: showChanges, - child: Text( - listedApps[index].app.releaseDate == - null - ? showChanges != null - ? tr('changes') - : '' - : DateFormat('yyyy-MM-dd') - .format(listedApps[index] - .app - .releaseDate!), - style: TextStyle( - fontStyle: FontStyle.italic, - decoration: showChanges != null - ? TextDecoration.underline - : TextDecoration.none), - )) - ], - ), - ], - ) - ], - )), - onTap: () { - if (selectedApps.isNotEmpty) { - toggleAppSelected(listedApps[index].app); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppPage(appId: listedApps[index].app.id)), - ); - } - }, - )); + return getSingleAppHorizTile(index); }, childCount: listedApps.length)) ])), persistentFooterButtons: appsProvider.apps.isEmpty ? null : [ - Row( - children: [ - selectedApps.isEmpty - ? TextButton.icon( - style: const ButtonStyle( - visualDensity: VisualDensity.compact), - onPressed: () { - selectThese(listedApps.map((e) => e.app).toList()); - }, - icon: Icon( - Icons.select_all_outlined, - color: Theme.of(context).colorScheme.primary, - ), - label: Text(listedApps.length.toString())) - : TextButton.icon( - style: const ButtonStyle( - visualDensity: VisualDensity.compact), - onPressed: () { - selectedApps.isEmpty - ? selectThese( - listedApps.map((e) => e.app).toList()) - : clearSelected(); - }, - icon: Icon( - selectedApps.isEmpty - ? Icons.select_all_outlined - : Icons.deselect_outlined, - color: Theme.of(context).colorScheme.primary, - ), - label: Text(selectedApps.length.toString())), - const VerticalDivider(), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - visualDensity: VisualDensity.compact, - onPressed: selectedApps.isEmpty - ? null - : () { - appsProvider.removeAppsWithModal( - context, selectedApps.toList()); - }, - tooltip: tr('removeSelectedApps'), - icon: const Icon(Icons.delete_outline_outlined), - ), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: appsProvider - .areDownloadsRunning() || - (existingUpdateIdsAllOrSelected - .isEmpty && - newInstallIdsAllOrSelected - .isEmpty && - trackOnlyUpdateIdsAllOrSelected - .isEmpty) - ? null - : () { - HapticFeedback.heavyImpact(); - List formItems = - []; - if (existingUpdateIdsAllOrSelected - .isNotEmpty) { - formItems.add(GeneratedFormSwitch( - 'updates', - label: tr('updateX', args: [ - plural( - 'apps', - existingUpdateIdsAllOrSelected - .length) - ]), - defaultValue: true)); - } - if (newInstallIdsAllOrSelected - .isNotEmpty) { - formItems.add(GeneratedFormSwitch( - 'installs', - label: tr('installX', args: [ - plural( - 'apps', - newInstallIdsAllOrSelected - .length) - ]), - defaultValue: - existingUpdateIdsAllOrSelected - .isNotEmpty)); - } - if (trackOnlyUpdateIdsAllOrSelected - .isNotEmpty) { - formItems.add(GeneratedFormSwitch( - 'trackonlies', - label: tr( - 'markXTrackOnlyAsUpdated', - args: [ - plural( - 'apps', - trackOnlyUpdateIdsAllOrSelected - .length) - ]), - defaultValue: - existingUpdateIdsAllOrSelected - .isNotEmpty || - newInstallIdsAllOrSelected - .isNotEmpty)); - } - showDialog?>( - context: context, - builder: (BuildContext ctx) { - var totalApps = existingUpdateIdsAllOrSelected - .length + - newInstallIdsAllOrSelected - .length + - trackOnlyUpdateIdsAllOrSelected - .length; - return GeneratedFormModal( - title: tr('changeX', args: [ - plural('apps', totalApps) - ]), - items: formItems - .map((e) => [e]) - .toList(), - initValid: true, - ); - }).then((values) { - if (values != null) { - if (values.isEmpty) { - values = - getDefaultValuesFromFormItems( - [formItems]); - } - bool shouldInstallUpdates = - values['updates'] == true; - bool shouldInstallNew = - values['installs'] == true; - bool shouldMarkTrackOnlies = - values['trackonlies'] == true; - (() async { - if (shouldInstallNew || - shouldInstallUpdates) { - await settingsProvider - .getInstallPermission(); - } - })() - .then((_) { - List toInstall = []; - if (shouldInstallUpdates) { - toInstall.addAll( - existingUpdateIdsAllOrSelected); - } - if (shouldInstallNew) { - toInstall.addAll( - newInstallIdsAllOrSelected); - } - if (shouldMarkTrackOnlies) { - toInstall.addAll( - trackOnlyUpdateIdsAllOrSelected); - } - appsProvider - .downloadAndInstallLatestApps( - toInstall, - globalNavigatorKey - .currentContext) - .catchError((e) { - showError(e, context); - }); - }); - } - }); - }, - tooltip: selectedApps.isEmpty - ? tr('installUpdateApps') - : tr('installUpdateSelectedApps'), - icon: const Icon( - Icons.file_download_outlined, - )), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: selectedApps.isEmpty - ? null - : () async { - try { - Set? preselected; - var showPrompt = false; - for (var element in selectedApps) { - var currentCats = - element.categories.toSet(); - if (preselected == null) { - preselected = currentCats; - } else { - if (!settingsProvider.setEqual( - currentCats, preselected)) { - showPrompt = true; - break; - } - } - } - var cont = true; - if (showPrompt) { - cont = await showDialog< - Map?>( - context: context, - builder: - (BuildContext ctx) { - return GeneratedFormModal( - title: tr('categorize'), - items: const [], - initValid: true, - message: tr( - 'selectedCategorizeWarning'), - ); - }) != - null; - } - if (cont) { - // ignore: use_build_context_synchronously - await showDialog< - Map?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('categorize'), - items: const [], - initValid: true, - singleNullReturnButton: - tr('continue'), - additionalWidgets: [ - CategoryEditorSelector( - preselected: !showPrompt - ? preselected ?? {} - : {}, - showLabelWhenNotEmpty: - false, - onSelected: - (categories) { - appsProvider.saveApps( - selectedApps - .map((e) { - e.categories = - categories; - return e; - }).toList()); - }, - ) - ], - ); - }); - } - } catch (err) { - showError(err, context); - } - }, - tooltip: tr('categorize'), - icon: const Icon(Icons.category_outlined), - ), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: selectedApps.isEmpty - ? null - : () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return AlertDialog( - scrollable: true, - content: Padding( - padding: - const EdgeInsets.only( - top: 6), - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceAround, - children: [ - IconButton( - onPressed: appsProvider - .areDownloadsRunning() - ? null - : () { - showDialog( - context: - context, - builder: - (BuildContext - ctx) { - return AlertDialog( - title: - Text(tr('markXSelectedAppsAsUpdated', args: [ - selectedApps.length.toString() - ])), - content: - Text( - tr('onlyWorksWithNonVersionDetectApps'), - style: const TextStyle(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(tr('no'))), - TextButton( - onPressed: () { - HapticFeedback.selectionClick(); - appsProvider.saveApps(selectedApps.map((a) { - if (a.installedVersion != null && a.additionalSettings['versionDetection'] != 'standardVersionDetection') { - a.installedVersion = a.latestVersion; - } - return a; - }).toList()); - - Navigator.of(context).pop(); - }, - child: Text(tr('yes'))) - ], - ); - }).whenComplete(() { - Navigator.of( - context) - .pop(); - }); - }, - tooltip: tr( - 'markSelectedAppsUpdated'), - icon: const Icon( - Icons.done)), - IconButton( - onPressed: () { - var pinStatus = selectedApps - .where((element) => - element - .pinned) - .isEmpty; - appsProvider - .saveApps( - selectedApps - .map( - (e) { - e.pinned = - pinStatus; - return e; - }).toList()); - Navigator.of( - context) - .pop(); - }, - 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: tr( - 'selectedAppURLsFromObtainium')); - Navigator.of( - context) - .pop(); - }, - tooltip: tr( - 'shareSelectedAppURLs'), - icon: const Icon( - Icons.share), - ), - IconButton( - onPressed: () { - showDialog( - context: - context, - builder: - (BuildContext - ctx) { - return GeneratedFormModal( - title: tr( - 'resetInstallStatusForSelectedAppsQuestion'), - items: const [], - initValid: - true, - message: tr( - 'installStatusOfXWillBeResetExplanation', - args: [ - plural( - 'app', - selectedApps.length) - ]), - ); - }).then((values) { - if (values != - null) { - appsProvider.saveApps( - selectedApps - .map( - (e) { - e.installedVersion = - null; - return e; - }).toList()); - } - }).whenComplete(() { - Navigator.of( - context) - .pop(); - }); - }, - tooltip: tr( - 'resetInstallStatus'), - icon: const Icon(Icons - .restore_page_outlined), - ), - ]), - ), - ); - }); - }, - tooltip: tr('more'), - icon: const Icon(Icons.more_horiz), - ), - ], - ))), - const VerticalDivider(), - IconButton( - visualDensity: VisualDensity.compact, - onPressed: () { - setState(() { - if (currentFilterIsUpdatesOnly) { - filter = AppsFilter(); - } else { - filter = updatesOnlyFilter; - } - }); - }, - tooltip: currentFilterIsUpdatesOnly - ? tr('removeOutdatedFilter') - : tr('showOutdatedOnly'), - icon: Icon( - currentFilterIsUpdatesOnly - ? Icons.update_disabled_rounded - : Icons.update_rounded, - color: Theme.of(context).colorScheme.primary, - ), - ), - TextButton.icon( - style: const ButtonStyle( - visualDensity: VisualDensity.compact), - label: Text( - filter.isIdenticalTo(neutralFilter, settingsProvider) - ? tr('filter') - : tr('filterActive'), - style: TextStyle( - fontWeight: filter.isIdenticalTo( - neutralFilter, settingsProvider) - ? FontWeight.normal - : FontWeight.bold), - ), - onPressed: () { - showDialog?>( - context: context, - builder: (BuildContext ctx) { - var vals = filter.toFormValuesMap(); - return GeneratedFormModal( - initValid: true, - title: tr('filterApps'), - items: [ - [ - GeneratedFormTextField('appName', - label: tr('appName'), - required: false, - defaultValue: vals['appName']), - GeneratedFormTextField('author', - label: tr('author'), - required: false, - defaultValue: vals['author']) - ], - [ - GeneratedFormSwitch('upToDateApps', - label: tr('upToDateApps'), - defaultValue: vals['upToDateApps']) - ], - [ - GeneratedFormSwitch('nonInstalledApps', - label: tr('nonInstalledApps'), - defaultValue: vals['nonInstalledApps']) - ] - ], - additionalWidgets: [ - const SizedBox( - height: 16, - ), - CategoryEditorSelector( - preselected: filter.categoryFilter, - onSelected: (categories) { - filter.categoryFilter = - categories.toSet(); - }, - ) - ], - ); - }).then((values) { - if (values != null) { - setState(() { - filter.setFormValuesFromMap(values); - }); - } - }); - }, - icon: const Icon(Icons.filter_list_rounded)) - ], - ), + getFilterButtonsRow(), ], ); } diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 71ec42c..9372f20 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -30,6 +30,7 @@ class _ImportExportPageState extends State { SourceProvider sourceProvider = SourceProvider(); var appsProvider = context.read(); var settingsProvider = context.read(); + var outlineButtonStyle = ButtonStyle( shape: MaterialStateProperty.all( StadiumBorder( @@ -101,6 +102,193 @@ class _ImportExportPageState extends State { }); } + runObtainiumExport() { + HapticFeedback.selectionClick(); + appsProvider.exportApps().then((String path) { + showError(tr('exportedTo', args: [path]), context); + }).catchError((e) { + showError(e, context); + }); + } + + runObtainiumImport() { + HapticFeedback.selectionClick(); + FilePicker.platform.pickFiles().then((result) { + setState(() { + importInProgress = true; + }); + if (result != null) { + String data = File(result.files.single.path!).readAsStringSync(); + try { + jsonDecode(data); + } catch (e) { + throw ObtainiumError(tr('invalidInput')); + } + appsProvider.importApps(data).then((value) { + var cats = settingsProvider.categories; + appsProvider.apps.forEach((key, value) { + for (var c in value.app.categories) { + if (!cats.containsKey(c)) { + cats[c] = generateRandomLightColor().value; + } + } + }); + settingsProvider.categories = cats; + showError(tr('importedX', args: [plural('apps', value)]), context); + }); + } else { + // User canceled the picker + } + }).catchError((e) { + showError(e, context); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + } + + runUrlImport() { + FilePicker.platform.pickFiles().then((result) { + if (result != null) { + urlListImport( + overrideInitValid: true, + initValue: RegExp('https?://[^"]+') + .allMatches( + File(result.files.single.path!).readAsStringSync()) + .map((e) => e.input.substring(e.start, e.end)) + .toSet() + .toList() + .where((url) { + try { + sourceProvider.getSource(url); + return true; + } catch (e) { + return false; + } + }).join('\n')); + } + }); + } + + runSourceSearch(AppSource source) { + () async { + var values = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('searchX', args: [source.name]), + items: [ + [ + GeneratedFormTextField('searchQuery', + label: tr('searchQuery')) + ] + ], + ); + }); + if (values != null && + (values['searchQuery'] as String?)?.isNotEmpty == true) { + setState(() { + importInProgress = true; + }); + var urlsWithDescriptions = + await source.search(values['searchQuery'] as String); + if (urlsWithDescriptions.isNotEmpty) { + var selectedUrls = + // ignore: use_build_context_synchronously + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return UrlSelectionModal( + urlsWithDescriptions: urlsWithDescriptions, + selectedByDefault: false, + ); + }); + if (selectedUrls != null && selectedUrls.isNotEmpty) { + var errors = await appsProvider.addAppsByURL(selectedUrls); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showError( + tr('importedX', args: [plural('app', selectedUrls.length)]), + context); + } else { + // ignore: use_build_context_synchronously + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: selectedUrls.length, errors: errors); + }); + } + } + } else { + throw ObtainiumError(tr('noResults')); + } + } + }() + .catchError((e) { + showError(e, context); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + } + + runMassSourceImport(MassAppUrlSource source) { + () async { + var values = await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: tr('importX', args: [source.name]), + items: source.requiredArgs + .map((e) => [GeneratedFormTextField(e, label: e)]) + .toList(), + ); + }); + if (values != null) { + setState(() { + importInProgress = true; + }); + var urlsWithDescriptions = await source.getUrlsWithDescriptions( + values.values.map((e) => e.toString()).toList()); + var selectedUrls = + // ignore: use_build_context_synchronously + await showDialog?>( + context: context, + builder: (BuildContext ctx) { + return UrlSelectionModal( + urlsWithDescriptions: urlsWithDescriptions); + }); + if (selectedUrls != null) { + var errors = await appsProvider.addAppsByURL(selectedUrls); + if (errors.isEmpty) { + // ignore: use_build_context_synchronously + showError( + tr('importedX', args: [plural('app', selectedUrls.length)]), + context); + } else { + // ignore: use_build_context_synchronously + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: selectedUrls.length, errors: errors); + }); + } + } + } + }() + .catchError((e) { + showError(e, context); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + } + return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView(slivers: [ @@ -120,18 +308,7 @@ class _ImportExportPageState extends State { onPressed: appsProvider.apps.isEmpty || importInProgress ? null - : () { - HapticFeedback.selectionClick(); - appsProvider - .exportApps() - .then((String path) { - showError( - tr('exportedTo', args: [path]), - context); - }).catchError((e) { - showError(e, context); - }); - }, + : runObtainiumExport, child: Text(tr('obtainiumExport')))), const SizedBox( width: 16, @@ -141,59 +318,7 @@ class _ImportExportPageState extends State { style: outlineButtonStyle, onPressed: importInProgress ? null - : () { - HapticFeedback.selectionClick(); - FilePicker.platform - .pickFiles() - .then((result) { - setState(() { - importInProgress = true; - }); - if (result != null) { - String data = File( - result.files.single.path!) - .readAsStringSync(); - try { - jsonDecode(data); - } catch (e) { - throw ObtainiumError( - tr('invalidInput')); - } - appsProvider - .importApps(data) - .then((value) { - var cats = - settingsProvider.categories; - appsProvider.apps - .forEach((key, value) { - for (var c - in value.app.categories) { - if (!cats.containsKey(c)) { - cats[c] = - generateRandomLightColor() - .value; - } - } - }); - settingsProvider.categories = - cats; - showError( - tr('importedX', args: [ - plural('apps', value) - ]), - context); - }); - } else { - // User canceled the picker - } - }).catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); - }, + : runObtainiumImport, child: Text(tr('obtainiumImport')))) ], ), @@ -216,49 +341,15 @@ class _ImportExportPageState extends State { height: 32, ), TextButton( - onPressed: importInProgress - ? null - : () { - urlListImport(); - }, + onPressed: + importInProgress ? null : urlListImport, child: Text( tr('importFromURLList'), )), const SizedBox(height: 8), TextButton( - onPressed: importInProgress - ? null - : () { - FilePicker.platform - .pickFiles() - .then((result) { - if (result != null) { - urlListImport( - overrideInitValid: true, - initValue: - RegExp('https?://[^"]+') - .allMatches(File(result - .files - .single - .path!) - .readAsStringSync()) - .map((e) => - e.input.substring( - e.start, e.end)) - .toSet() - .toList() - .where((url) { - try { - sourceProvider - .getSource(url); - return true; - } catch (e) { - return false; - } - }).join('\n')); - } - }); - }, + onPressed: + importInProgress ? null : runUrlImport, child: Text( tr('importFromURLsInFile'), )), @@ -275,106 +366,7 @@ class _ImportExportPageState extends State { onPressed: importInProgress ? null : () { - () async { - var values = await showDialog< - Map?>( - context: context, - builder: - (BuildContext ctx) { - return GeneratedFormModal( - title: tr('searchX', - args: [ - source.name - ]), - items: [ - [ - GeneratedFormTextField( - 'searchQuery', - label: tr( - 'searchQuery')) - ] - ], - ); - }); - if (values != null && - (values['searchQuery'] - as String?) - ?.isNotEmpty == - true) { - setState(() { - importInProgress = true; - }); - var urlsWithDescriptions = - await source.search( - values['searchQuery'] - as String); - if (urlsWithDescriptions - .isNotEmpty) { - var selectedUrls = - // ignore: use_build_context_synchronously - await showDialog< - List< - String>?>( - context: context, - builder: - (BuildContext - ctx) { - return UrlSelectionModal( - urlsWithDescriptions: - urlsWithDescriptions, - selectedByDefault: - false, - ); - }); - if (selectedUrls != - null && - selectedUrls - .isNotEmpty) { - var errors = - await appsProvider - .addAppsByURL( - selectedUrls); - if (errors.isEmpty) { - // ignore: use_build_context_synchronously - showError( - tr('importedX', - args: [ - plural( - 'app', - selectedUrls - .length) - ]), - context); - } else { - // ignore: use_build_context_synchronously - showDialog( - context: context, - builder: - (BuildContext - ctx) { - return ImportErrorDialog( - urlsLength: - selectedUrls - .length, - errors: - errors); - }); - } - } - } else { - throw ObtainiumError( - tr('noResults')); - } - } - }() - .catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); + runSourceSearch(source); }, child: Text( tr('searchX', args: [source.name]))) @@ -390,93 +382,7 @@ class _ImportExportPageState extends State { onPressed: importInProgress ? null : () { - () async { - var values = await showDialog< - Map?>( - context: context, - builder: - (BuildContext ctx) { - return GeneratedFormModal( - title: tr('importX', - args: [ - source.name - ]), - items: - source - .requiredArgs - .map( - (e) => [ - GeneratedFormTextField(e, - label: e) - ]) - .toList(), - ); - }); - if (values != null) { - setState(() { - importInProgress = true; - }); - var urlsWithDescriptions = - await source - .getUrlsWithDescriptions( - values.values - .map((e) => - e.toString()) - .toList()); - var selectedUrls = - // ignore: use_build_context_synchronously - await showDialog< - List?>( - context: context, - builder: - (BuildContext - ctx) { - return UrlSelectionModal( - urlsWithDescriptions: - urlsWithDescriptions); - }); - if (selectedUrls != null) { - var errors = - await appsProvider - .addAppsByURL( - selectedUrls); - if (errors.isEmpty) { - // ignore: use_build_context_synchronously - showError( - tr('importedX', - args: [ - plural( - 'app', - selectedUrls - .length) - ]), - context); - } else { - // ignore: use_build_context_synchronously - showDialog( - context: context, - builder: - (BuildContext - ctx) { - return ImportErrorDialog( - urlsLength: - selectedUrls - .length, - errors: - errors); - }); - } - } - } - }() - .catchError((e) { - showError(e, context); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); + runMassSourceImport(source); }, child: Text( tr('importX', args: [source.name]))) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index b0c9675..cf19488 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -73,6 +73,18 @@ List generateStandardVersionRegExStrings() { List standardVersionRegExStrings = generateStandardVersionRegExStrings(); +Set findStandardFormatsForVersion(String version, bool strict) { + // If !strict, even a substring match is valid + Set results = {}; + for (var pattern in standardVersionRegExStrings) { + if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}') + .hasMatch(version)) { + results.add(pattern); + } + } + return results; +} + class AppsProvider with ChangeNotifier { // In memory App state (should always be kept in sync with local storage versions) Map apps = {}; @@ -472,94 +484,113 @@ class AppsProvider with ChangeNotifier { return res; } - // If the App says it is installed but installedInfo is null, set it to not installed - // If there is any other mismatch between installedInfo and installedVersion, try reconciling them intelligently - // If that fails, just set it to the actual version string (all we can do at that point) - // Don't save changes, just return the object if changes were made (else null) + bool isVersionDetectionPossible(AppInMemory? app) { + return app?.app.additionalSettings['trackOnly'] != true && + app?.installedInfo?.versionName != null && + app?.app.installedVersion != null && + reconcileVersionDifferences( + app!.installedInfo!.versionName!, app.app.installedVersion!) != + null; + } + + // Given an App and it's on-device info... + // Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { var modded = false; var trackOnly = app.additionalSettings['trackOnly'] == true; var noVersionDetection = app.additionalSettings['versionDetection'] != 'standardVersionDetection'; + // FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL if (installedInfo == null && app.installedVersion != null && !trackOnly) { + // App says it's installed but isn't really (and isn't track only) - set to not installed app.installedVersion = null; modded = true; } else if (installedInfo?.versionName != null && app.installedVersion == null) { + // App says it's not installed but really is - set to installed and use real package versionName app.installedVersion = installedInfo!.versionName; modded = true; - } else if (installedInfo?.versionName != null && + } + // SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL + if (installedInfo?.versionName != null && installedInfo!.versionName != app.installedVersion && !noVersionDetection) { - String? correctedInstalledVersion = reconcileRealAndInternalVersions( + // App's reported version and real version don't match (and it uses standard version detection) + // If they share a standard format (and are still different under it), update the reported version accordingly + var correctedInstalledVersion = reconcileVersionDifferences( installedInfo.versionName!, app.installedVersion!); - if (correctedInstalledVersion != null) { - app.installedVersion = correctedInstalledVersion; + if (correctedInstalledVersion?.key == false) { + app.installedVersion = correctedInstalledVersion!.value; modded = true; } } + // THIRD, RECONCILE THE APP'S REPORTED INSTALLED AND LATEST VERSIONS if (app.installedVersion != null && app.installedVersion != app.latestVersion && !noVersionDetection) { - app.installedVersion = reconcileRealAndInternalVersions( - app.installedVersion!, app.latestVersion, - matchMode: true) ?? - app.installedVersion; + // App's reported installed and latest versions don't match (and it uses standard version detection) + // If they share a standard format, make sure the App's reported installed version uses that format + var correctedInstalledVersion = + reconcileVersionDifferences(app.installedVersion!, app.latestVersion); + if (correctedInstalledVersion?.key == true) { + app.installedVersion = correctedInstalledVersion!.value; + modded = true; + } + } + // FOURTH, DISABLE VERSION DETECTION IF ENABLED AND THE REPORTED/REAL INSTALLED VERSIONS ARE NOT STANDARDIZED + if (installedInfo != null && + !isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) { + app.additionalSettings['versionDetection'] = 'noVersionDetection'; + logs.add('Could not reconcile version formats for: ${app.id}'); modded = true; } + // if (app.installedVersion != null && + // app.additionalSettings['versionDetection'] == + // 'standardVersionDetection') { + // var correctedInstalledVersion = + // reconcileVersionDifferences(app.installedVersion!, app.latestVersion); + // if (correctedInstalledVersion == null) { + // app.additionalSettings['versionDetection'] = 'noVersionDetection'; + // logs.add('Could not reconcile version formats for: ${app.id}'); + // modded = true; + // } + // } + return modded ? app : null; } - String? reconcileRealAndInternalVersions( - String realVersion, String internalVersion, - {bool matchMode = false}) { - // 1. If one or both of these can't be converted to a "standard" format, return null (leave as is) - // 2. If both have a "standard" format under which they are equal, return null (leave as is) - // 3. If both have a "standard" format in common but are unequal, return realVersion (this means it was changed externally) - // If in matchMode, the outcomes of rules 2 and 3 are reversed, and the "real" version is not matched strictly - // Matchmode to be used when comparing internal install version and internal latest version - - bool doStringsMatchUnderRegEx( - String pattern, String value1, String value2) { - var r = RegExp(pattern); - var m1 = r.firstMatch(value1); - var m2 = r.firstMatch(value2); - return m1 != null && m2 != null - ? value1.substring(m1.start, m1.end) == - value2.substring(m2.start, m2.end) - : false; - } - - Set findStandardFormatsForVersion(String version, bool strict) { - Set results = {}; - for (var pattern in standardVersionRegExStrings) { - if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}') - .hasMatch(version)) { - results.add(pattern); - } - } - return results; - } - - var realStandardVersionFormats = - findStandardFormatsForVersion(realVersion, true); - var internalStandardVersionFormats = - findStandardFormatsForVersion(internalVersion, false); + MapEntry? reconcileVersionDifferences( + String templateVersion, String comparisonVersion) { + // Returns null if the versions don't share a common standard format + // Returns if they share a common format and are equal + // Returns if they share a common format but are not equal + // templateVersion must fully match a standard format, while comparisonVersion can have a substring match + var templateVersionFormats = + findStandardFormatsForVersion(templateVersion, true); + var comparisonVersionFormats = + findStandardFormatsForVersion(comparisonVersion, false); var commonStandardFormats = - realStandardVersionFormats.intersection(internalStandardVersionFormats); + templateVersionFormats.intersection(comparisonVersionFormats); if (commonStandardFormats.isEmpty) { - return null; // Incompatible; no "enhanced detection" + return null; } for (String pattern in commonStandardFormats) { - if (doStringsMatchUnderRegEx(pattern, internalVersion, realVersion)) { - return matchMode - ? internalVersion - : null; // Enhanced detection says no change + if (doStringsMatchUnderRegEx( + pattern, comparisonVersion, templateVersion)) { + return MapEntry(true, comparisonVersion); } } - return matchMode - ? null - : realVersion; // Enhanced detection says something changed + return MapEntry(false, templateVersion); + } + + bool doStringsMatchUnderRegEx(String pattern, String value1, String value2) { + var r = RegExp(pattern); + var m1 = r.firstMatch(value1); + var m2 = r.firstMatch(value2); + return m1 != null && m2 != null + ? value1.substring(m1.start, m1.end) == + value2.substring(m2.start, m2.end) + : false; } Future loadApps() async { diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 0f3eb16..be18176 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -21,7 +21,6 @@ import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/vlc.dart'; -import 'package:obtainium/app_sources/whatsapp.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart'; @@ -111,16 +110,16 @@ class App { // Convert bool style version detection options to dropdown style if (additionalSettings['noVersionDetection'] == true) { additionalSettings['versionDetection'] = 'noVersionDetection'; - } - if (additionalSettings['releaseDateAsVersion'] == true) { - additionalSettings['versionDetection'] = 'releaseDateAsVersion'; - additionalSettings.remove('releaseDateAsVersion'); - } - if (additionalSettings['noVersionDetection'] != null) { - additionalSettings.remove('noVersionDetection'); - } - if (additionalSettings['releaseDateAsVersion'] != null) { - additionalSettings.remove('releaseDateAsVersion'); + if (additionalSettings['releaseDateAsVersion'] == true) { + additionalSettings['versionDetection'] = 'releaseDateAsVersion'; + additionalSettings.remove('releaseDateAsVersion'); + } + if (additionalSettings['noVersionDetection'] != null) { + additionalSettings.remove('noVersionDetection'); + } + if (additionalSettings['releaseDateAsVersion'] != null) { + additionalSettings.remove('releaseDateAsVersion'); + } } // Ensure additionalSettings are correctly typed for (var item in formItems) { diff --git a/pubspec.lock b/pubspec.lock index 66602aa..b58e952 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -409,10 +409,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" path_provider_android: dependency: transitive description: @@ -425,10 +425,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "12eee51abdf4d34c590f043f45073adbb45514a108bd9db4491547a2fd891059" + sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_linux: dependency: transitive description: @@ -473,10 +473,10 @@ packages: dependency: transitive description: name: permission_handler_apple - sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 url: "https://pub.dev" source: hosted - version: "9.0.7" + version: "9.0.8" permission_handler_platform_interface: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 + sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.0.20" shared_preferences_android: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" shared_preferences_web: dependency: transitive description: @@ -710,10 +710,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 url: "https://pub.dev" source: hosted - version: "6.0.25" + version: "6.0.26" url_launcher_ios: dependency: transitive description: @@ -806,10 +806,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: ab12479f7a0cf112b9420c36aaf206a1ca47cd60cd42de74a4be2e97a697587b + sha256: d601aba11ad8d4481e17a34a76fa1d30dee92dcbbe2c58b0df3120e9453099c7 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e2e1dea..82a25b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.11.15+136 # When changing this, update the tag in main() accordingly +version: 0.11.16+138 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0'