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'