mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 21:13:28 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			376 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			376 lines
		
	
	
		
			17 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/providers/apps_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;
 | |
| 
 | |
|   String userInput = '';
 | |
|   String searchQuery = '';
 | |
|   AppSource? pickedSource;
 | |
|   Map<String, dynamic> additionalSettings = {};
 | |
|   bool additionalSettingsValid = true;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     SourceProvider sourceProvider = SourceProvider();
 | |
|     AppsProvider appsProvider = context.read<AppsProvider>();
 | |
| 
 | |
|     changeUserInput(String input, bool valid, bool isBuilding) {
 | |
|       userInput = input;
 | |
|       fn() {
 | |
|         var source = valid ? sourceProvider.getSource(userInput) : null;
 | |
|         if (pickedSource.runtimeType != source.runtimeType) {
 | |
|           pickedSource = source;
 | |
|           additionalSettings = source != null
 | |
|               ? getDefaultValuesFromFormItems(
 | |
|                   source.combinedAppSpecificSettingFormItems)
 | |
|               : {};
 | |
|           additionalSettingsValid = source != null
 | |
|               ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
 | |
|               : true;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (isBuilding) {
 | |
|         fn();
 | |
|       } else {
 | |
|         setState(() {
 | |
|           fn();
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     addApp({bool resetUserInputAfter = false}) async {
 | |
|       setState(() {
 | |
|         gettingAppInfo = true;
 | |
|       });
 | |
|       var settingsProvider = context.read<SettingsProvider>();
 | |
|       () async {
 | |
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
 | |
|         var userPickedNoVersionDetection =
 | |
|             additionalSettings['noVersionDetection'] == true;
 | |
|         var cont = true;
 | |
|         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
 | |
|             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 (userPickedNoVersionDetection &&
 | |
|             await showDialog(
 | |
|                     context: context,
 | |
|                     builder: (BuildContext ctx) {
 | |
|                       return GeneratedFormModal(
 | |
|                         title: tr('disableVersionDetection'),
 | |
|                         items: const [],
 | |
|                         message: tr('noVersionDetectionExplanation'),
 | |
|                       );
 | |
|                     }) ==
 | |
|                 null) {
 | |
|           cont = false;
 | |
|         }
 | |
|         if (cont) {
 | |
|           HapticFeedback.selectionClick();
 | |
|           var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
 | |
|           App app = await sourceProvider.getApp(
 | |
|               pickedSource!, userInput, additionalSettings,
 | |
|               trackOnlyOverride: trackOnly,
 | |
|               noVersionDetectionOverride: userPickedNoVersionDetection);
 | |
|           if (!trackOnly) {
 | |
|             await settingsProvider.getInstallPermission();
 | |
|           }
 | |
|           // Only download the APK here if you need to for the package ID
 | |
|           if (sourceProvider.isTempId(app.id) &&
 | |
|               app.additionalSettings['trackOnly'] != true) {
 | |
|             // ignore: use_build_context_synchronously
 | |
|             var apkUrl = await appsProvider.confirmApkUrl(app, context);
 | |
|             if (apkUrl == null) {
 | |
|               throw ObtainiumError(tr('cancelled'));
 | |
|             }
 | |
|             app.preferredApkIndex = app.apkUrls.indexOf(apkUrl);
 | |
|             // ignore: use_build_context_synchronously
 | |
|             var downloadedApk = await appsProvider.downloadApp(
 | |
|                 app, globalNavigatorKey.currentContext);
 | |
|             app.id = downloadedApk.appId;
 | |
|           }
 | |
|           if (appsProvider.apps.containsKey(app.id)) {
 | |
|             throw ObtainiumError(tr('appAlreadyAdded'));
 | |
|           }
 | |
|           if (app.additionalSettings['trackOnly'] == true) {
 | |
|             app.installedVersion = app.latestVersion;
 | |
|           }
 | |
|           await appsProvider.saveApps([app]);
 | |
| 
 | |
|           return app;
 | |
|         }
 | |
|       }()
 | |
|           .then((app) {
 | |
|         if (app != null) {
 | |
|           Navigator.push(context,
 | |
|               MaterialPageRoute(builder: (context) => AppPage(appId: app.id)));
 | |
|         }
 | |
|       }).catchError((e) {
 | |
|         showError(e, context);
 | |
|       }).whenComplete(() {
 | |
|         setState(() {
 | |
|           gettingAppInfo = false;
 | |
|           if (resetUserInputAfter) {
 | |
|             changeUserInput('', false, true);
 | |
|           }
 | |
|         });
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return Scaffold(
 | |
|         backgroundColor: Theme.of(context).colorScheme.surface,
 | |
|         body: CustomScrollView(slivers: <Widget>[
 | |
|           CustomAppBar(title: tr('addApp')),
 | |
|           SliverFillRemaining(
 | |
|             child: Padding(
 | |
|                 padding: const EdgeInsets.all(16),
 | |
|                 child: Column(
 | |
|                     crossAxisAlignment: CrossAxisAlignment.stretch,
 | |
|                     children: [
 | |
|                       Row(
 | |
|                         children: [
 | |
|                           Expanded(
 | |
|                               child: GeneratedForm(
 | |
|                                   items: [
 | |
|                                 [
 | |
|                                   GeneratedFormTextField('appSourceURL',
 | |
|                                       label: tr('appSourceURL'),
 | |
|                                       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: gettingAppInfo ||
 | |
|                                           pickedSource == null ||
 | |
|                                           (pickedSource!
 | |
|                                                   .combinedAppSpecificSettingFormItems
 | |
|                                                   .isNotEmpty &&
 | |
|                                               !additionalSettingsValid)
 | |
|                                       ? null
 | |
|                                       : addApp,
 | |
|                                   child: Text(tr('add')))
 | |
|                         ],
 | |
|                       ),
 | |
|                       if (sourceProvider.sources
 | |
|                               .where((e) => e.canSearch)
 | |
|                               .isNotEmpty &&
 | |
|                           pickedSource == null &&
 | |
|                           userInput.isEmpty)
 | |
|                         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) {
 | |
|                                       setState(() {
 | |
|                                         searchQuery =
 | |
|                                             values['searchSomeSources']!.trim();
 | |
|                                       });
 | |
|                                     }
 | |
|                                   }),
 | |
|                             ),
 | |
|                             const SizedBox(
 | |
|                               width: 16,
 | |
|                             ),
 | |
|                             ElevatedButton(
 | |
|                                 onPressed: searchQuery.isEmpty || gettingAppInfo
 | |
|                                     ? null
 | |
|                                     : () {
 | |
|                                         Future.wait(sourceProvider.sources
 | |
|                                                 .where((e) => e.canSearch)
 | |
|                                                 .map((e) =>
 | |
|                                                     e.search(searchQuery)))
 | |
|                                             .then((results) async {
 | |
|                                           // Interleave results instead of simple reduce
 | |
|                                           Map<String, 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++;
 | |
|                                           }
 | |
|                                           List<String>? selectedUrls = res
 | |
|                                                   .isEmpty
 | |
|                                               ? []
 | |
|                                               : await showDialog<List<String>?>(
 | |
|                                                   context: context,
 | |
|                                                   builder: (BuildContext ctx) {
 | |
|                                                     return UrlSelectionModal(
 | |
|                                                       urlsWithDescriptions: res,
 | |
|                                                       selectedByDefault: false,
 | |
|                                                       onlyOneSelectionAllowed:
 | |
|                                                           true,
 | |
|                                                     );
 | |
|                                                   });
 | |
|                                           if (selectedUrls != null &&
 | |
|                                               selectedUrls.isNotEmpty) {
 | |
|                                             changeUserInput(
 | |
|                                                 selectedUrls[0], true, true);
 | |
|                                             addApp(resetUserInputAfter: true);
 | |
|                                           }
 | |
|                                         }).catchError((e) {
 | |
|                                           showError(e, context);
 | |
|                                         });
 | |
|                                       },
 | |
|                                 child: Text(tr('search')))
 | |
|                           ],
 | |
|                         ),
 | |
|                       if (pickedSource != null &&
 | |
|                           (pickedSource!
 | |
|                               .combinedAppSpecificSettingFormItems.isNotEmpty))
 | |
|                         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(
 | |
|                                 items: pickedSource!
 | |
|                                     .combinedAppSpecificSettingFormItems,
 | |
|                                 onValueChanges: (values, valid, isBuilding) {
 | |
|                                   if (!isBuilding) {
 | |
|                                     setState(() {
 | |
|                                       additionalSettings = values;
 | |
|                                       additionalSettingsValid = valid;
 | |
|                                     });
 | |
|                                   }
 | |
|                                 }),
 | |
|                           ],
 | |
|                         )
 | |
|                       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()
 | |
|                             ])),
 | |
|                       const SizedBox(
 | |
|                         height: 8,
 | |
|                       ),
 | |
|                     ])),
 | |
|           )
 | |
|         ]));
 | |
|   }
 | |
| }
 |