import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equations/equations.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:obtainium/components/custom_app_bar.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'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/native_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:shizuku_apk_installer/shizuku_apk_installer.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { List updateIntervalNodes = [ 15, 30, 60, 120, 180, 360, 720, 1440, 4320, 10080, 20160, 43200, ]; int updateInterval = 0; late SplineInterpolation updateIntervalInterpolator; // 🤓 String updateIntervalLabel = tr('neverManualOnly'); bool showIntervalLabel = true; final Map, String> colorsNameMap = , String>{ ColorTools.createPrimarySwatch(obtainiumThemeColor): 'Obtainium', }; void initUpdateIntervalInterpolator() { List nodes = []; for (final (index, element) in updateIntervalNodes.indexed) { nodes.add( InterpolationNode(x: index.toDouble() + 1, y: element.toDouble()), ); } updateIntervalInterpolator = SplineInterpolation(nodes: nodes); } void processIntervalSliderValue(double val) { if (val < 0.5) { updateInterval = 0; updateIntervalLabel = tr('neverManualOnly'); return; } int valInterpolated = 0; if (val < 1) { valInterpolated = 15; } else { valInterpolated = updateIntervalInterpolator.compute(val).round(); } if (valInterpolated < 60) { updateInterval = valInterpolated; updateIntervalLabel = plural('minute', valInterpolated); } else if (valInterpolated < 8 * 60) { int valRounded = (valInterpolated / 15).floor() * 15; updateInterval = valRounded; updateIntervalLabel = plural('hour', valRounded ~/ 60); int mins = valRounded % 60; if (mins != 0) updateIntervalLabel += " ${plural('minute', mins)}"; } else if (valInterpolated < 24 * 60) { int valRounded = (valInterpolated / 30).floor() * 30; updateInterval = valRounded; updateIntervalLabel = plural('hour', valRounded / 60); } else if (valInterpolated < 7 * 24 * 60) { int valRounded = (valInterpolated / (12 * 60)).floor() * 12 * 60; updateInterval = valRounded; updateIntervalLabel = plural('day', valRounded / (24 * 60)); } else { int valRounded = (valInterpolated / (24 * 60)).floor() * 24 * 60; updateInterval = valRounded; updateIntervalLabel = plural('day', valRounded ~/ (24 * 60)); } } @override Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); SourceProvider sourceProvider = SourceProvider(); if (settingsProvider.prefs == null) settingsProvider.initializeSettings(); initUpdateIntervalInterpolator(); processIntervalSliderValue(settingsProvider.updateIntervalSliderVal); var followSystemThemeExplanation = FutureBuilder( builder: (ctx, val) { return ((val.data?.version.sdkInt ?? 30) < 29) ? Text( tr('followSystemThemeExplanation'), style: Theme.of(context).textTheme.labelSmall, ) : const SizedBox.shrink(); }, future: DeviceInfoPlugin().androidInfo, ); Future colorPickerDialog() async { return ColorPicker( color: settingsProvider.themeColor, onColorChanged: (Color color) => setState(() => settingsProvider.themeColor = color), actionButtons: const ColorPickerActionButtons( okButton: true, closeButton: true, dialogActionButtons: false, ), pickersEnabled: const { ColorPickerType.both: false, ColorPickerType.primary: false, ColorPickerType.accent: false, ColorPickerType.bw: false, ColorPickerType.custom: true, ColorPickerType.wheel: true, }, pickerTypeLabels: { ColorPickerType.custom: tr('standard'), ColorPickerType.wheel: tr('custom'), }, title: Text( tr('selectX', args: [tr('colour')]), style: Theme.of(context).textTheme.titleLarge, ), wheelDiameter: 192, wheelSquareBorderRadius: 32, width: 48, height: 48, borderRadius: 24, spacing: 8, runSpacing: 8, enableShadesSelection: false, customColorSwatchesAndNames: colorsNameMap, showMaterialName: true, showColorName: true, materialNameTextStyle: Theme.of(context).textTheme.bodySmall, colorNameTextStyle: Theme.of(context).textTheme.bodySmall, copyPasteBehavior: const ColorPickerCopyPasteBehavior( longPressMenu: true, ), ).showPickerDialog( context, transitionBuilder: ( BuildContext context, Animation a1, Animation a2, Widget widget, ) { final double curvedValue = Curves.easeInCubic.transform(a1.value); return Transform( alignment: Alignment.center, transform: Matrix4.diagonal3Values(curvedValue, curvedValue, 1), child: Opacity(opacity: curvedValue, child: widget), ); }, transitionDuration: const Duration(milliseconds: 250), ); } var colorPicker = ListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text(tr('selectX', args: [tr('colour')])), subtitle: Text( "${ColorTools.nameThatColor(settingsProvider.themeColor)} " "(${ColorTools.materialNameAndCode(settingsProvider.themeColor, colorSwatchNameMap: colorsNameMap)})", ), trailing: ColorIndicator( width: 40, height: 40, borderRadius: 20, color: settingsProvider.themeColor, onSelectFocus: false, onSelect: () async { final Color colorBeforeDialog = settingsProvider.themeColor; if (!(await colorPickerDialog())) { setState(() { settingsProvider.themeColor = colorBeforeDialog; }); } }, ), ); var useMaterialThemeSwitch = FutureBuilder( builder: (ctx, val) { return ((val.data?.version.sdkInt ?? 0) >= 31) ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('useMaterialYou'))), Switch( value: settingsProvider.useMaterialYou, onChanged: (value) { settingsProvider.useMaterialYou = value; }, ), ], ) : const SizedBox.shrink(); }, future: DeviceInfoPlugin().androidInfo, ); var sortDropdown = DropdownButtonFormField( isExpanded: true, decoration: InputDecoration(labelText: tr('appSortBy')), value: settingsProvider.sortColumn, items: [ DropdownMenuItem( value: SortColumnSettings.authorName, child: Text(tr('authorName')), ), DropdownMenuItem( value: SortColumnSettings.nameAuthor, child: Text(tr('nameAuthor')), ), DropdownMenuItem( value: SortColumnSettings.added, child: Text(tr('asAdded')), ), DropdownMenuItem( value: SortColumnSettings.releaseDate, child: Text(tr('releaseDate')), ), ], onChanged: (value) { if (value != null) { settingsProvider.sortColumn = value; } }, ); var orderDropdown = DropdownButtonFormField( isExpanded: true, decoration: InputDecoration(labelText: tr('appSortOrder')), value: settingsProvider.sortOrder, items: [ DropdownMenuItem( value: SortOrderSettings.ascending, child: Text(tr('ascending')), ), DropdownMenuItem( value: SortOrderSettings.descending, child: Text(tr('descending')), ), ], onChanged: (value) { if (value != null) { settingsProvider.sortOrder = value; } }, ); var localeDropdown = DropdownButtonFormField( decoration: InputDecoration(labelText: tr('language')), value: settingsProvider.forcedLocale, items: [ DropdownMenuItem(value: null, child: Text(tr('followSystem'))), ...supportedLocales.map( (e) => DropdownMenuItem(value: e.key, child: Text(e.value)), ), ], onChanged: (value) { settingsProvider.forcedLocale = value; if (value != null) { context.setLocale(value); } else { settingsProvider.resetLocaleSafe(context); } }, ); var intervalSlider = Slider( value: settingsProvider.updateIntervalSliderVal, max: updateIntervalNodes.length.toDouble(), divisions: updateIntervalNodes.length * 20, label: updateIntervalLabel, onChanged: (double value) { setState(() { settingsProvider.updateIntervalSliderVal = value; processIntervalSliderValue(value); }); }, onChangeStart: (double value) { setState(() { showIntervalLabel = false; }); }, onChangeEnd: (double value) { setState(() { showIntervalLabel = true; settingsProvider.updateInterval = updateInterval; }); }, ); var sourceSpecificFields = sourceProvider.sources.map((e) { if (e.sourceConfigSettingFormItems.isNotEmpty) { return GeneratedForm( items: e.sourceConfigSettingFormItems.map((e) { e.defaultValue = settingsProvider.getSettingString(e.key); return [e]; }).toList(), onValueChanges: (values, valid, isBuilding) { if (valid && !isBuilding) { values.forEach((key, value) { settingsProvider.setSettingString(key, value); }); } }, ); } else { return Container(); } }); const height8 = SizedBox(height: 8); const height16 = SizedBox(height: 16); const height32 = SizedBox(height: 32); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView( slivers: [ CustomAppBar(title: tr('settings')), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: settingsProvider.prefs == null ? const SizedBox() : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tr('updates'), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), //intervalDropdown, height16, if (showIntervalLabel) SizedBox( child: Text( "${tr('bgUpdateCheckInterval')}: $updateIntervalLabel", ), ) else const SizedBox(height: 16), intervalSlider, FutureBuilder( builder: (ctx, val) { return (settingsProvider.updateInterval > 0) && (((val.data?.version.sdkInt ?? 0) >= 30) || settingsProvider.useShizuku) ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text( tr( 'foregroundServiceExplanation', ), ), ), Switch( value: settingsProvider.useFGService, onChanged: (value) { settingsProvider.useFGService = value; }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text( tr('enableBackgroundUpdates'), ), ), Switch( value: settingsProvider .enableBackgroundUpdates, onChanged: (value) { settingsProvider .enableBackgroundUpdates = value; }, ), ], ), height8, Text( tr('backgroundUpdateReqsExplanation'), style: Theme.of( context, ).textTheme.labelSmall, ), Text( tr('backgroundUpdateLimitsExplanation'), style: Theme.of( context, ).textTheme.labelSmall, ), height8, if (settingsProvider .enableBackgroundUpdates) Column( children: [ height16, Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Flexible( child: Text( tr('bgUpdatesOnWiFiOnly'), ), ), Switch( value: settingsProvider .bgUpdatesOnWiFiOnly, onChanged: (value) { settingsProvider .bgUpdatesOnWiFiOnly = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ Flexible( child: Text( tr( 'bgUpdatesWhileChargingOnly', ), ), ), Switch( value: settingsProvider .bgUpdatesWhileChargingOnly, onChanged: (value) { settingsProvider .bgUpdatesWhileChargingOnly = value; }, ), ], ), ], ), ], ) : const SizedBox.shrink(); }, future: DeviceInfoPlugin().androidInfo, ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('checkOnStart'))), Switch( value: settingsProvider.checkOnStart, onChanged: (value) { settingsProvider.checkOnStart = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('checkUpdateOnDetailPage')), ), Switch( value: settingsProvider.checkUpdateOnDetailPage, onChanged: (value) { settingsProvider.checkUpdateOnDetailPage = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text( tr('onlyCheckInstalledOrTrackOnlyApps'), ), ), Switch( value: settingsProvider .onlyCheckInstalledOrTrackOnlyApps, onChanged: (value) { settingsProvider .onlyCheckInstalledOrTrackOnlyApps = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('removeOnExternalUninstall')), ), Switch( value: settingsProvider.removeOnExternalUninstall, onChanged: (value) { settingsProvider.removeOnExternalUninstall = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('parallelDownloads'))), Switch( value: settingsProvider.parallelDownloads, onChanged: (value) { settingsProvider.parallelDownloads = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( tr('beforeNewInstallsShareToAppVerifier'), ), GestureDetector( onTap: () { launchUrlString( 'https://github.com/soupslurpr/AppVerifier', mode: LaunchMode.externalApplication, ); }, child: Text( tr('about'), style: const TextStyle( decoration: TextDecoration.underline, fontSize: 12, ), ), ), ], ), ), Switch( value: settingsProvider .beforeNewInstallsShareToAppVerifier, onChanged: (value) { settingsProvider .beforeNewInstallsShareToAppVerifier = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('useShizuku'))), Switch( value: settingsProvider.useShizuku, onChanged: (useShizuku) { if (useShizuku) { ShizukuApkInstaller.checkPermission().then(( resCode, ) { settingsProvider.useShizuku = resCode! .startsWith('granted'); switch (resCode) { case 'binder_not_found': showError( ObtainiumError( tr('shizukuBinderNotFound'), ), context, ); case 'old_shizuku': showError( ObtainiumError(tr('shizukuOld')), context, ); case 'old_android_with_adb': showError( ObtainiumError( tr('shizukuOldAndroidWithADB'), ), context, ); case 'denied': showError( ObtainiumError(tr('cancelled')), context, ); } }); } else { settingsProvider.useShizuku = false; } }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('shizukuPretendToBeGooglePlay')), ), Switch( value: settingsProvider.shizukuPretendToBeGooglePlay, onChanged: (value) { settingsProvider.shizukuPretendToBeGooglePlay = value; }, ), ], ), height32, Text( tr('sourceSpecific'), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), ...sourceSpecificFields, height32, Text( tr('appearance'), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), DropdownButtonFormField( decoration: InputDecoration(labelText: tr('theme')), value: settingsProvider.theme, items: [ DropdownMenuItem( value: ThemeSettings.system, child: Text(tr('followSystem')), ), DropdownMenuItem( value: ThemeSettings.light, child: Text(tr('light')), ), DropdownMenuItem( value: ThemeSettings.dark, child: Text(tr('dark')), ), ], onChanged: (value) { if (value != null) { settingsProvider.theme = value; } }, ), height8, if (settingsProvider.theme == ThemeSettings.system) followSystemThemeExplanation, height16, if (settingsProvider.theme != ThemeSettings.light) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('useBlackTheme'))), Switch( value: settingsProvider.useBlackTheme, onChanged: (value) { settingsProvider.useBlackTheme = value; }, ), ], ), height8, useMaterialThemeSwitch, if (!settingsProvider.useMaterialYou) colorPicker, Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: sortDropdown), const SizedBox(width: 16), Expanded(child: orderDropdown), ], ), height16, localeDropdown, FutureBuilder( builder: (ctx, val) { return (val.data?.version.sdkInt ?? 0) >= 34 ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('useSystemFont')), ), Switch( value: settingsProvider.useSystemFont, onChanged: (useSystemFont) { if (useSystemFont) { NativeFeatures.loadSystemFont() .then((val) { settingsProvider .useSystemFont = true; }); } else { settingsProvider.useSystemFont = false; } }, ), ], ), ], ) : const SizedBox.shrink(); }, future: DeviceInfoPlugin().androidInfo, ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('showWebInAppView'))), Switch( value: settingsProvider.showAppWebpage, onChanged: (value) { settingsProvider.showAppWebpage = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('pinUpdates'))), Switch( value: settingsProvider.pinUpdates, onChanged: (value) { settingsProvider.pinUpdates = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('moveNonInstalledAppsToBottom')), ), Switch( value: settingsProvider.buryNonInstalled, onChanged: (value) { settingsProvider.buryNonInstalled = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('groupByCategory'))), Switch( value: settingsProvider.groupByCategory, onChanged: (value) { settingsProvider.groupByCategory = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('dontShowTrackOnlyWarnings')), ), Switch( value: settingsProvider.hideTrackOnlyWarning, onChanged: (value) { settingsProvider.hideTrackOnlyWarning = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text(tr('dontShowAPKOriginWarnings')), ), Switch( value: settingsProvider.hideAPKOriginWarning, onChanged: (value) { settingsProvider.hideAPKOriginWarning = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('disablePageTransitions'))), Switch( value: settingsProvider.disablePageTransitions, onChanged: (value) { settingsProvider.disablePageTransitions = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('reversePageTransitions'))), Switch( value: settingsProvider.reversePageTransitions, onChanged: settingsProvider.disablePageTransitions ? null : (value) { settingsProvider.reversePageTransitions = value; }, ), ], ), height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: Text(tr('highlightTouchTargets'))), Switch( value: settingsProvider.highlightTouchTargets, onChanged: (value) { settingsProvider.highlightTouchTargets = value; }, ), ], ), height32, Text( tr('categories'), style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), height16, const CategoryEditorSelector( showLabelWhenNotEmpty: false, ), ], ), ), ), SliverToBoxAdapter( child: Column( children: [ const Divider(height: 32), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( onPressed: () { launchUrlString( settingsProvider.sourceUrl, mode: LaunchMode.externalApplication, ); }, icon: const Icon(Icons.code), tooltip: tr('appSource'), ), IconButton( onPressed: () { launchUrlString( 'https://wiki.obtainium.imranr.dev/', mode: LaunchMode.externalApplication, ); }, icon: const Icon(Icons.help_outline_rounded), tooltip: tr('wiki'), ), IconButton( onPressed: () { launchUrlString( 'https://apps.obtainium.imranr.dev/', mode: LaunchMode.externalApplication, ); }, icon: const Icon(Icons.apps_rounded), tooltip: tr('crowdsourcedConfigsLabel'), ), IconButton( onPressed: () { context.read().get().then((logs) { if (logs.isEmpty) { showMessage(ObtainiumError(tr('noLogs')), context); } else { showDialog( context: context, builder: (BuildContext ctx) { return const LogsDialog(); }, ); } }); }, icon: const Icon(Icons.bug_report_outlined), tooltip: tr('appLogs'), ), ], ), const SizedBox(height: 16), ], ), ), ], ), ); } } class LogsDialog extends StatefulWidget { const LogsDialog({super.key}); @override State createState() => _LogsDialogState(); } class _LogsDialogState extends State { String? logString; List days = [7, 5, 4, 3, 2, 1]; @override Widget build(BuildContext context) { var logsProvider = context.read(); void filterLogs(int days) { logsProvider .get(after: DateTime.now().subtract(Duration(days: days))) .then((value) { setState(() { String l = value.map((e) => e.toString()).join('\n\n'); logString = l.isNotEmpty ? l : tr('noLogs'); }); }); } if (logString == null) { filterLogs(days.first); } return AlertDialog( scrollable: true, title: Text(tr('appLogs')), content: Column( children: [ DropdownButtonFormField( value: days.first, items: days .map( (e) => DropdownMenuItem(value: e, child: Text(plural('day', e))), ) .toList(), onChanged: (d) { filterLogs(d ?? 7); }, ), const SizedBox(height: 32), Text(logString ?? ''), ], ), actions: [ TextButton( onPressed: () async { var cont = (await showDialog?>( context: context, builder: (BuildContext ctx) { return GeneratedFormModal( title: tr('appLogs'), items: const [], initValid: true, message: tr('removeFromObtainium'), ); }, )) != null; if (cont) { logsProvider.clear(); Navigator.of(context).pop(); } }, child: Text(tr('remove')), ), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(tr('close')), ), TextButton( onPressed: () { Share.share(logString ?? '', subject: tr('appLogs')); Navigator.of(context).pop(); }, child: Text(tr('share')), ), ], ); } } class CategoryEditorSelector extends StatefulWidget { final void Function(List categories)? onSelected; final bool singleSelect; final Set preselected; final WrapAlignment alignment; final bool showLabelWhenNotEmpty; const CategoryEditorSelector({ super.key, this.onSelected, this.singleSelect = false, this.preselected = const {}, this.alignment = WrapAlignment.start, this.showLabelWhenNotEmpty = true, }); @override State createState() => _CategoryEditorSelectorState(); } class _CategoryEditorSelectorState extends State { Map> storedValues = {}; @override Widget build(BuildContext context) { var settingsProvider = context.watch(); var appsProvider = context.watch(); storedValues = settingsProvider.categories.map( (key, value) => MapEntry( key, MapEntry( value, storedValues[key]?.value ?? widget.preselected.contains(key), ), ), ); return GeneratedForm( items: [ [ GeneratedFormTagInput( 'categories', label: tr('categories'), emptyMessage: tr('noCategories'), defaultValue: storedValues, alignment: widget.alignment, deleteConfirmationMessage: MapEntry( tr('deleteCategoriesQuestion'), tr('categoryDeleteWarning'), ), singleSelect: widget.singleSelect, showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty, ), ], ], onValueChanges: ((values, valid, isBuilding) { if (!isBuilding) { storedValues = values['categories'] as Map>; settingsProvider.setCategories( storedValues.map((key, value) => MapEntry(key, value.key)), appsProvider: appsProvider, ); if (widget.onSelected != null) { widget.onSelected!( storedValues.keys.where((k) => storedValues[k]!.value).toList(), ); } } }), ); } }