mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-26 11:13:46 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			589 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			589 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.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/pages/app.dart';
 | |
| import 'package:obtainium/pages/import_export.dart';
 | |
| import 'package:obtainium/pages/settings.dart';
 | |
| import 'package:obtainium/providers/apps_provider.dart';
 | |
| import 'package:obtainium/providers/notifications_provider.dart';
 | |
| import 'package:obtainium/providers/settings_provider.dart';
 | |
| import 'package:obtainium/providers/source_provider.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:url_launcher/url_launcher_string.dart';
 | |
| 
 | |
| class AddAppPage extends StatefulWidget {
 | |
|   const AddAppPage({super.key});
 | |
| 
 | |
|   @override
 | |
|   State<AddAppPage> createState() => AddAppPageState();
 | |
| }
 | |
| 
 | |
| class AddAppPageState extends State<AddAppPage> {
 | |
|   bool gettingAppInfo = false;
 | |
|   bool searching = false;
 | |
| 
 | |
|   String userInput = '';
 | |
|   String searchQuery = '';
 | |
|   String? pickedSourceOverride;
 | |
|   AppSource? pickedSource;
 | |
|   Map<String, dynamic> additionalSettings = {};
 | |
|   bool additionalSettingsValid = true;
 | |
|   bool inferAppIdIfOptional = true;
 | |
|   List<String> pickedCategories = [];
 | |
|   int urlInputKey = 0;
 | |
|   SourceProvider sourceProvider = SourceProvider();
 | |
| 
 | |
|   linkFn(String input) {
 | |
|     try {
 | |
|       if (input.isEmpty) {
 | |
|         throw UnsupportedURLError();
 | |
|       }
 | |
|       sourceProvider.getSource(input);
 | |
|       changeUserInput(input, true, false, updateUrlInput: true);
 | |
|     } catch (e) {
 | |
|       showError(e, context);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   changeUserInput(String input, bool valid, bool isBuilding,
 | |
|       {bool updateUrlInput = false}) {
 | |
|     userInput = input;
 | |
|     if (!isBuilding) {
 | |
|       setState(() {
 | |
|         if (updateUrlInput) {
 | |
|           urlInputKey++;
 | |
|         }
 | |
|         var prevHost = pickedSource?.hosts.isNotEmpty == true
 | |
|             ? pickedSource?.hosts[0]
 | |
|             : null;
 | |
|         var source = valid
 | |
|             ? sourceProvider.getSource(userInput,
 | |
|                 overrideSource: pickedSourceOverride)
 | |
|             : null;
 | |
|         if (pickedSource.runtimeType != source.runtimeType ||
 | |
|             (prevHost != null && prevHost != source?.hosts[0])) {
 | |
|           pickedSource = source;
 | |
|           additionalSettings = source != null
 | |
|               ? getDefaultValuesFromFormItems(
 | |
|                   source.combinedAppSpecificSettingFormItems)
 | |
|               : {};
 | |
|           additionalSettingsValid = source != null
 | |
|               ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
 | |
|               : true;
 | |
|           inferAppIdIfOptional = true;
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     AppsProvider appsProvider = context.read<AppsProvider>();
 | |
|     SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | |
|     NotificationsProvider notificationsProvider =
 | |
|         context.read<NotificationsProvider>();
 | |
| 
 | |
|     bool doingSomething = gettingAppInfo || searching;
 | |
| 
 | |
|     Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
 | |
|         {bool ignoreHideSetting = false}) async {
 | |
|       var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
 | |
|       if (useTrackOnly &&
 | |
|           (!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
 | |
|         // ignore: use_build_context_synchronously
 | |
|         var values = await showDialog(
 | |
|             context: context,
 | |
|             builder: (BuildContext ctx) {
 | |
|               return GeneratedFormModal(
 | |
|                 initValid: true,
 | |
|                 title: tr('xIsTrackOnly', args: [
 | |
|                   pickedSource!.enforceTrackOnly ? tr('source') : tr('app')
 | |
|                 ]),
 | |
|                 items: [
 | |
|                   [GeneratedFormSwitch('hide', label: tr('dontShowAgain'))]
 | |
|                 ],
 | |
|                 message:
 | |
|                     '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
 | |
|               );
 | |
|             });
 | |
|         if (values != null) {
 | |
|           settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
 | |
|         }
 | |
|         return useTrackOnly && values != null;
 | |
|       } else {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     getReleaseDateAsVersionConfirmationIfNeeded(
 | |
|         bool userPickedTrackOnly) async {
 | |
|       return (!(additionalSettings['releaseDateAsVersion'] == true &&
 | |
|           // 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;
 | |
|       });
 | |
|       try {
 | |
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
 | |
|         App? app;
 | |
|         if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
 | |
|             (await getReleaseDateAsVersionConfirmationIfNeeded(
 | |
|                 userPickedTrackOnly))) {
 | |
|           var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
 | |
|           app = await sourceProvider.getApp(
 | |
|               pickedSource!, userInput.trim(), additionalSettings,
 | |
|               trackOnlyOverride: trackOnly,
 | |
|               sourceIsOverriden: pickedSourceOverride != null,
 | |
|               inferAppIdIfOptional: inferAppIdIfOptional);
 | |
|           // Only download the APK here if you need to for the package ID
 | |
|           if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
 | |
|             // ignore: use_build_context_synchronously
 | |
|             var apkUrl =
 | |
|                 await appsProvider.confirmAppFileUrl(app, context, false);
 | |
|             if (apkUrl == null) {
 | |
|               throw ObtainiumError(tr('cancelled'));
 | |
|             }
 | |
|             app.preferredApkIndex =
 | |
|                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
 | |
|             // ignore: use_build_context_synchronously
 | |
|             var downloadedArtifact = await appsProvider.downloadApp(
 | |
|                 app, globalNavigatorKey.currentContext,
 | |
|                 notificationsProvider: notificationsProvider);
 | |
|             DownloadedApk? downloadedFile;
 | |
|             DownloadedXApkDir? downloadedDir;
 | |
|             if (downloadedArtifact is DownloadedApk) {
 | |
|               downloadedFile = downloadedArtifact;
 | |
|             } else {
 | |
|               downloadedDir = downloadedArtifact as DownloadedXApkDir;
 | |
|             }
 | |
|             app.id = downloadedFile?.appId ?? downloadedDir!.appId;
 | |
|           }
 | |
|           if (appsProvider.apps.containsKey(app.id)) {
 | |
|             throw ObtainiumError(tr('appAlreadyAdded'));
 | |
|           }
 | |
|           if (app.additionalSettings['trackOnly'] == true ||
 | |
|               app.additionalSettings['versionDetection'] != true) {
 | |
|             app.installedVersion = app.latestVersion;
 | |
|           }
 | |
|           app.categories = pickedCategories;
 | |
|           await appsProvider.saveApps([app], onlyIfExists: false);
 | |
|         }
 | |
|         if (app != null) {
 | |
|           Navigator.push(globalNavigatorKey.currentContext ?? context,
 | |
|               MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)));
 | |
|         }
 | |
|       } catch (e) {
 | |
|         showError(e, context);
 | |
|       } finally {
 | |
|         setState(() {
 | |
|           gettingAppInfo = false;
 | |
|           if (resetUserInputAfter) {
 | |
|             changeUserInput('', false, true);
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     Widget getUrlInputRow() => Row(
 | |
|           children: [
 | |
|             Expanded(
 | |
|                 child: GeneratedForm(
 | |
|                     key: Key(urlInputKey.toString()),
 | |
|                     items: [
 | |
|                       [
 | |
|                         GeneratedFormTextField('appSourceURL',
 | |
|                             label: tr('appSourceURL'),
 | |
|                             defaultValue: userInput,
 | |
|                             additionalValidators: [
 | |
|                               (value) {
 | |
|                                 try {
 | |
|                                   sourceProvider
 | |
|                                       .getSource(value ?? '',
 | |
|                                           overrideSource: pickedSourceOverride)
 | |
|                                       .standardizeUrl(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({bool filtered = true}) async {
 | |
|       setState(() {
 | |
|         searching = true;
 | |
|       });
 | |
|       var sourceStrings = <String, List<String>>{};
 | |
|       sourceProvider.sources
 | |
|           .where((e) => e.canSearch && !e.excludeFromMassSearch)
 | |
|           .forEach((s) {
 | |
|         sourceStrings[s.name] = [s.name];
 | |
|       });
 | |
|       try {
 | |
|         var searchSources = await showDialog<List<String>?>(
 | |
|                 context: context,
 | |
|                 builder: (BuildContext ctx) {
 | |
|                   return SelectionModal(
 | |
|                     title: tr('selectX', args: [plural('source', 2)]),
 | |
|                     entries: sourceStrings,
 | |
|                     selectedByDefault: true,
 | |
|                     onlyOneSelectionAllowed: false,
 | |
|                     titlesAreLinks: false,
 | |
|                     deselectThese: settingsProvider.searchDeselected,
 | |
|                   );
 | |
|                 }) ??
 | |
|             [];
 | |
|         if (searchSources.isNotEmpty) {
 | |
|           settingsProvider.searchDeselected = sourceStrings.keys
 | |
|               .where((s) => !searchSources.contains(s))
 | |
|               .toList();
 | |
|           var results = await Future.wait(sourceProvider.sources
 | |
|               .where((e) => searchSources.contains(e.name))
 | |
|               .map((e) async {
 | |
|             try {
 | |
|               return await e.search(searchQuery);
 | |
|             } catch (err) {
 | |
|               if (err is! CredsNeededError) {
 | |
|                 rethrow;
 | |
|               } else {
 | |
|                 err.unexpected = true;
 | |
|                 showError(err, context);
 | |
|                 return <String, List<String>>{};
 | |
|               }
 | |
|             }
 | |
|           }));
 | |
| 
 | |
|           // Interleave results instead of simple reduce
 | |
|           Map<String, List<String>> 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++;
 | |
|           }
 | |
|           if (res.isEmpty) {
 | |
|             throw ObtainiumError(tr('noResults'));
 | |
|           }
 | |
|           List<String>? selectedUrls = res.isEmpty
 | |
|               ? []
 | |
|               // ignore: use_build_context_synchronously
 | |
|               : await showDialog<List<String>?>(
 | |
|                   context: context,
 | |
|                   builder: (BuildContext ctx) {
 | |
|                     return SelectionModal(
 | |
|                       entries: res,
 | |
|                       selectedByDefault: false,
 | |
|                       onlyOneSelectionAllowed: true,
 | |
|                     );
 | |
|                   });
 | |
|           if (selectedUrls != null && selectedUrls.isNotEmpty) {
 | |
|             changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
 | |
|           }
 | |
|         }
 | |
|       } catch (e) {
 | |
|         showError(e, context);
 | |
|       } finally {
 | |
|         setState(() {
 | |
|           searching = false;
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     Widget getHTMLSourceOverrideDropdown() => Column(children: [
 | |
|           Row(
 | |
|             children: [
 | |
|               Expanded(
 | |
|                   child: GeneratedForm(
 | |
|                 items: [
 | |
|                   [
 | |
|                     GeneratedFormDropdown(
 | |
|                         'overrideSource',
 | |
|                         defaultValue: '',
 | |
|                         [
 | |
|                           MapEntry('', tr('none')),
 | |
|                           ...sourceProvider.sources.map(
 | |
|                               (s) => MapEntry(s.runtimeType.toString(), s.name))
 | |
|                         ],
 | |
|                         label: tr('overrideSource'))
 | |
|                   ]
 | |
|                 ],
 | |
|                 onValueChanges: (values, valid, isBuilding) {
 | |
|                   fn() {
 | |
|                     pickedSourceOverride = (values['overrideSource'] == null ||
 | |
|                             values['overrideSource'] == '')
 | |
|                         ? null
 | |
|                         : values['overrideSource'];
 | |
|                   }
 | |
| 
 | |
|                   if (!isBuilding) {
 | |
|                     setState(() {
 | |
|                       fn();
 | |
|                     });
 | |
|                   } else {
 | |
|                     fn();
 | |
|                   }
 | |
|                   changeUserInput(userInput, valid, isBuilding);
 | |
|                 },
 | |
|               ))
 | |
|             ],
 | |
|           ),
 | |
|           const SizedBox(
 | |
|             height: 16,
 | |
|           )
 | |
|         ]);
 | |
| 
 | |
|     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,
 | |
|             ),
 | |
|             searching
 | |
|                 ? const CircularProgressIndicator()
 | |
|                 : ElevatedButton(
 | |
|                     onPressed: searchQuery.isEmpty || doingSomething
 | |
|                         ? null
 | |
|                         : () {
 | |
|                             runSearch();
 | |
|                           },
 | |
|                     child: Text(tr('search')))
 | |
|           ],
 | |
|         );
 | |
| 
 | |
|     Widget getAdditionalOptsCol() => Column(
 | |
|           crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|           children: [
 | |
|             const SizedBox(
 | |
|               height: 16,
 | |
|             ),
 | |
|             Text(
 | |
|                 tr('additionalOptsFor',
 | |
|                     args: [pickedSource?.name ?? tr('source')]),
 | |
|                 style: TextStyle(
 | |
|                     color: Theme.of(context).colorScheme.primary,
 | |
|                     fontWeight: FontWeight.bold)),
 | |
|             const SizedBox(
 | |
|               height: 16,
 | |
|             ),
 | |
|             GeneratedForm(
 | |
|                 key: Key(pickedSource.runtimeType.toString()),
 | |
|                 items: [
 | |
|                   ...pickedSource!.combinedAppSpecificSettingFormItems,
 | |
|                   ...(pickedSourceOverride != null
 | |
|                       ? pickedSource!.sourceConfigSettingFormItems
 | |
|                           .map((e) => [e])
 | |
|                       : [])
 | |
|                 ],
 | |
|                 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;
 | |
|                     }),
 | |
|               ],
 | |
|             ),
 | |
|             if (pickedSource != null && pickedSource!.appIdInferIsOptional)
 | |
|               GeneratedForm(
 | |
|                   key: const Key('inferAppIdIfOptional'),
 | |
|                   items: [
 | |
|                     [
 | |
|                       GeneratedFormSwitch('inferAppIdIfOptional',
 | |
|                           label: tr('tryInferAppIdFromCode'),
 | |
|                           defaultValue: inferAppIdIfOptional)
 | |
|                     ]
 | |
|                   ],
 | |
|                   onValueChanges: (values, valid, isBuilding) {
 | |
|                     if (!isBuilding) {
 | |
|                       setState(() {
 | |
|                         inferAppIdIfOptional = values['inferAppIdIfOptional'];
 | |
|                       });
 | |
|                     }
 | |
|                   }),
 | |
|           ],
 | |
|         );
 | |
| 
 | |
|     Widget getSourcesListWidget() => Padding(
 | |
|           padding: const EdgeInsets.all(16),
 | |
|           child: Row(
 | |
|             children: [
 | |
|               GestureDetector(
 | |
|                   onTap: () {
 | |
|                     showDialog(
 | |
|                       context: context,
 | |
|                       builder: (context) {
 | |
|                         return GeneratedFormModal(
 | |
|                           singleNullReturnButton: tr('ok'),
 | |
|                           title: tr('supportedSources'),
 | |
|                           items: const [],
 | |
|                           additionalWidgets: [
 | |
|                             ...sourceProvider.sources.map(
 | |
|                               (e) => Padding(
 | |
|                                   padding:
 | |
|                                       const EdgeInsets.symmetric(vertical: 4),
 | |
|                                   child: GestureDetector(
 | |
|                                       onTap: e.hosts.isNotEmpty
 | |
|                                           ? () {
 | |
|                                               launchUrlString(
 | |
|                                                   'https://${e.hosts[0]}',
 | |
|                                                   mode: LaunchMode
 | |
|                                                       .externalApplication);
 | |
|                                             }
 | |
|                                           : null,
 | |
|                                       child: Text(
 | |
|                                         '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
 | |
|                                         style: TextStyle(
 | |
|                                             decoration: e.hosts.isNotEmpty
 | |
|                                                 ? TextDecoration.underline
 | |
|                                                 : TextDecoration.none),
 | |
|                                       ))),
 | |
|                             ),
 | |
|                             const SizedBox(
 | |
|                               height: 16,
 | |
|                             ),
 | |
|                             Text(
 | |
|                               '${tr('note')}:',
 | |
|                               style:
 | |
|                                   const TextStyle(fontWeight: FontWeight.bold),
 | |
|                             ),
 | |
|                             const SizedBox(
 | |
|                               height: 4,
 | |
|                             ),
 | |
|                             Text(tr('selfHostedNote',
 | |
|                                 args: [tr('overrideSource')])),
 | |
|                           ],
 | |
|                         );
 | |
|                       },
 | |
|                     );
 | |
|                   },
 | |
|                   child: Text(
 | |
|                     tr('supportedSources'),
 | |
|                     style: const TextStyle(
 | |
|                         fontWeight: FontWeight.bold,
 | |
|                         decoration: TextDecoration.underline,
 | |
|                         fontStyle: FontStyle.italic),
 | |
|                   ))
 | |
|             ],
 | |
|           ),
 | |
|         );
 | |
| 
 | |
|     return Scaffold(
 | |
|         backgroundColor: Theme.of(context).colorScheme.surface,
 | |
|         bottomNavigationBar:
 | |
|             pickedSource == null ? getSourcesListWidget() : null,
 | |
|         body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
 | |
|           CustomAppBar(title: tr('addApp')),
 | |
|           SliverToBoxAdapter(
 | |
|             child: Padding(
 | |
|                 padding: const EdgeInsets.all(16),
 | |
|                 child: Column(
 | |
|                     mainAxisSize: MainAxisSize.min,
 | |
|                     crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|                     children: [
 | |
|                       getUrlInputRow(),
 | |
|                       const SizedBox(
 | |
|                         height: 16,
 | |
|                       ),
 | |
|                       if (pickedSource != null) getHTMLSourceOverrideDropdown(),
 | |
|                       if (shouldShowSearchBar()) getSearchBarRow(),
 | |
|                       if (pickedSource != null)
 | |
|                         FutureBuilder(
 | |
|                             builder: (ctx, val) {
 | |
|                               return val.data != null && val.data!.isNotEmpty
 | |
|                                   ? Text(
 | |
|                                       val.data!,
 | |
|                                       style:
 | |
|                                           Theme.of(context).textTheme.bodySmall,
 | |
|                                     )
 | |
|                                   : const SizedBox();
 | |
|                             },
 | |
|                             future: pickedSource?.getSourceNote()),
 | |
|                       if (pickedSource != null) getAdditionalOptsCol(),
 | |
|                     ])),
 | |
|           )
 | |
|         ]));
 | |
|   }
 | |
| }
 |