mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 13:33:28 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			v0.11.15-b
			...
			v0.11.17-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6f6a25511b | ||
|  | 4e17bbcfd1 | ||
|  | 814e269d1d | ||
|  | 6b7d962b87 | ||
|  | 9fba747802 | 
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)", | ||||
|     "versionDetection": "Versionserkennung", | ||||
|     "standardVersionDetection": "Standardversionserkennung", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "App entfernen?", | ||||
|         "other": "App entfernen?" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "Import from URLs in File (like OPML)", | ||||
|     "versionDetection": "Version Detection", | ||||
|     "standardVersionDetection": "Standard version detection", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remove App?", | ||||
|         "other": "Remove Apps?" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)", | ||||
|     "versionDetection": "تشخیص نسخه", | ||||
|     "standardVersionDetection": "تشخیص نسخه استاندارد", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "برنامه حذف شود؟", | ||||
|         "other": "برنامه ها حذف شوند؟" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "Importer à partir d'URL dans un fichier (comme OPML)", | ||||
|     "versionDetection": "Détection des versions", | ||||
|     "standardVersionDetection": "Détection de version standard", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Supprimer l'application ?", | ||||
|         "other": "Supprimer les applications ?" | ||||
|   | ||||
| @@ -219,6 +219,7 @@ | ||||
|     "importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)", | ||||
|     "versionDetection": "Verzió érzékelés", | ||||
|     "standardVersionDetection": "Alapért. verzió érzékelés", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Eltávolítja az alkalmazást?", | ||||
|         "other": "Eltávolítja az alkalmazást?" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "Importa da URL in file (come OPML)", | ||||
|     "versionDetection": "Rilevamento di versione", | ||||
|     "standardVersionDetection": "Rilevamento di versione standard", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Rimuovere l'App?", | ||||
|         "other": "Rimuovere le App?" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "ファイル(OPMLなど)内のURLからインポート", | ||||
|     "versionDetection": "バージョン検出", | ||||
|     "standardVersionDetection": "標準のバージョン検出", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "アプリを削除しますか?", | ||||
|         "other": "アプリを削除しますか?" | ||||
|   | ||||
| @@ -220,6 +220,7 @@ | ||||
|     "importFromURLsInFile": "Import from URLs in File (like OPML)", | ||||
|     "versionDetection": "Version Detection", | ||||
|     "standardVersionDetection": "Standard version detection", | ||||
|     "groupByCategory": "Group by Category", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "删除应用?", | ||||
|         "other": "删除应用?" | ||||
|   | ||||
| @@ -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<String, dynamic> 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) { | ||||
|     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(); | ||||
|     } | ||||
|       return APKDetails( | ||||
|           version, | ||||
|           ['https://mullvad.net/download/app/apk/latest'], | ||||
|           AppNames(name, 'Mullvad-VPN')); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,7 @@ class GeneratedFormTextField extends GeneratedFormItem { | ||||
|  | ||||
| class GeneratedFormDropdown extends GeneratedFormItem { | ||||
|   late List<MapEntry<String, String>>? opts; | ||||
|   List<String>? disabledOptKeys; | ||||
|  | ||||
|   GeneratedFormDropdown( | ||||
|     String key, | ||||
| @@ -55,6 +56,7 @@ class GeneratedFormDropdown extends GeneratedFormItem { | ||||
|     String label = 'Input', | ||||
|     List<Widget> belowWidgets = const [], | ||||
|     String defaultValue = '', | ||||
|     this.disabledOptKeys, | ||||
|     List<String? Function(String? value)> additionalValidators = const [], | ||||
|   }) : super(key, | ||||
|             label: label, | ||||
| @@ -225,10 +227,15 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|           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; | ||||
|   | ||||
| @@ -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.17'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
|   | ||||
| @@ -33,10 +33,10 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|   bool additionalSettingsValid = true; | ||||
|   List<String> pickedCategories = []; | ||||
|   int searchnum = 0; | ||||
|   SourceProvider sourceProvider = SourceProvider(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||
|  | ||||
|     bool doingSomething = gettingAppInfo || searching; | ||||
| @@ -64,15 +64,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     addApp({bool resetUserInputAfter = false}) async { | ||||
|       setState(() { | ||||
|         gettingAppInfo = true; | ||||
|       }); | ||||
|       var settingsProvider = context.read<SettingsProvider>(); | ||||
|       () async { | ||||
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == true; | ||||
|         var cont = true; | ||||
|         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && | ||||
|     getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async { | ||||
|       return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && | ||||
|           // ignore: use_build_context_synchronously | ||||
|           await showDialog( | ||||
|                   context: context, | ||||
| @@ -88,10 +81,13 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                           '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', | ||||
|                     ); | ||||
|                   }) == | ||||
|                 null) { | ||||
|           cont = false; | ||||
|               null)); | ||||
|     } | ||||
|         if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' && | ||||
|  | ||||
|     getReleaseDateAsVersionConfirmationIfNeeded( | ||||
|         bool userPickedTrackOnly) async { | ||||
|       return (!(additionalSettings['versionDetection'] == | ||||
|               'releaseDateAsVersion' && | ||||
|           // ignore: use_build_context_synchronously | ||||
|           await showDialog( | ||||
|                   context: context, | ||||
| @@ -102,27 +98,22 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                       message: tr('releaseDateAsVersionExplanation'), | ||||
|                     ); | ||||
|                   }) == | ||||
|                 null) { | ||||
|           cont = false; | ||||
|               null)); | ||||
|     } | ||||
|         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(); | ||||
|  | ||||
|     addApp({bool resetUserInputAfter = false}) async { | ||||
|       setState(() { | ||||
|         gettingAppInfo = true; | ||||
|       }); | ||||
|       try { | ||||
|         var settingsProvider = context.read<SettingsProvider>(); | ||||
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == true; | ||||
|         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,38 +141,24 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           } | ||||
|           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); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     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( | ||||
|     Widget getUrlInputRow() => Row( | ||||
|           children: [ | ||||
|             Expanded( | ||||
|                 child: GeneratedForm( | ||||
| @@ -197,8 +174,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                   sourceProvider | ||||
|                                       .getSource(value ?? '') | ||||
|                                       .standardizeURL( | ||||
|                                                         preStandardizeUrl( | ||||
|                                                             value ?? '')); | ||||
|                                           preStandardizeUrl(value ?? '')); | ||||
|                                 } catch (e) { | ||||
|                                   return e is String | ||||
|                                       ? e | ||||
| @@ -212,8 +188,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                       ] | ||||
|                     ], | ||||
|                     onValueChanges: (values, valid, isBuilding) { | ||||
|                                     changeUserInput(values['appSourceURL']!, | ||||
|                                         valid, isBuilding); | ||||
|                       changeUserInput( | ||||
|                           values['appSourceURL']!, valid, isBuilding); | ||||
|                     })), | ||||
|             const SizedBox( | ||||
|               width: 16, | ||||
| @@ -223,47 +199,85 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                 : ElevatedButton( | ||||
|                     onPressed: doingSomething || | ||||
|                             pickedSource == null || | ||||
|                                           (pickedSource! | ||||
|                                                   .combinedAppSpecificSettingFormItems | ||||
|                             (pickedSource!.combinedAppSpecificSettingFormItems | ||||
|                                     .isNotEmpty && | ||||
|                                 !additionalSettingsValid) | ||||
|                         ? null | ||||
|                                       : addApp, | ||||
|                         : () { | ||||
|                             HapticFeedback.selectionClick(); | ||||
|                             addApp(); | ||||
|                           }, | ||||
|                     child: Text(tr('add'))) | ||||
|           ], | ||||
|                       ), | ||||
|                       if (sourceProvider.sources | ||||
|         ); | ||||
|  | ||||
|     runSearch() async { | ||||
|       setState(() { | ||||
|         searching = true; | ||||
|       }); | ||||
|       try { | ||||
|         var results = await Future.wait(sourceProvider.sources | ||||
|             .where((e) => e.canSearch) | ||||
|                               .isNotEmpty && | ||||
|             .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 | ||||
|             ? [] | ||||
|             // ignore: use_build_context_synchronously | ||||
|             : 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, 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) | ||||
|                         const SizedBox( | ||||
|                           height: 16, | ||||
|                         ), | ||||
|                       if (sourceProvider.sources | ||||
|                               .where((e) => e.canSearch) | ||||
|                               .isNotEmpty && | ||||
|                           pickedSource == null && | ||||
|                           userInput.isEmpty) | ||||
|                         Row( | ||||
|         userInput.isEmpty; | ||||
|  | ||||
|     Widget getSearchBarRow() => Row( | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: GeneratedForm( | ||||
|                   items: [ | ||||
|                     [ | ||||
|                                       GeneratedFormTextField( | ||||
|                                           'searchSomeSources', | ||||
|                                           label: tr('searchSomeSourcesLabel'), | ||||
|                                           required: false), | ||||
|                       GeneratedFormTextField('searchSomeSources', | ||||
|                           label: tr('searchSomeSourcesLabel'), required: false), | ||||
|                     ] | ||||
|                   ], | ||||
|                   onValueChanges: (values, valid, isBuilding) { | ||||
|                                     if (values.isNotEmpty && | ||||
|                                         valid && | ||||
|                                         !isBuilding) { | ||||
|                     if (values.isNotEmpty && valid && !isBuilding) { | ||||
|                       setState(() { | ||||
|                                         searchQuery = | ||||
|                                             values['searchSomeSources']!.trim(); | ||||
|                         searchQuery = values['searchSomeSources']!.trim(); | ||||
|                       }); | ||||
|                     } | ||||
|                   }), | ||||
| @@ -275,61 +289,13 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                 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<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, false, | ||||
|                                                 isSearch: true); | ||||
|                                           } | ||||
|                                         }).catchError((e) { | ||||
|                                           showError(e, context); | ||||
|                                         }).whenComplete(() { | ||||
|                                           setState(() { | ||||
|                                             searching = false; | ||||
|                                           }); | ||||
|                                         }); | ||||
|                         runSearch(); | ||||
|                       }, | ||||
|                 child: Text(tr('search'))) | ||||
|           ], | ||||
|                         ), | ||||
|                       if (pickedSource != null) | ||||
|                         Column( | ||||
|         ); | ||||
|  | ||||
|     Widget getAdditionalOptsCol() => Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: [ | ||||
|             const Divider( | ||||
| @@ -338,16 +304,13 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|             Text( | ||||
|                 tr('additionalOptsFor', | ||||
|                     args: [pickedSource?.name ?? tr('source')]), | ||||
|                                 style: TextStyle( | ||||
|                                     color: | ||||
|                                         Theme.of(context).colorScheme.primary)), | ||||
|                 style: TextStyle(color: Theme.of(context).colorScheme.primary)), | ||||
|             const SizedBox( | ||||
|               height: 16, | ||||
|             ), | ||||
|             GeneratedForm( | ||||
|                 key: Key(pickedSource.runtimeType.toString()), | ||||
|                                 items: pickedSource! | ||||
|                                     .combinedAppSpecificSettingFormItems, | ||||
|                 items: pickedSource!.combinedAppSpecificSettingFormItems, | ||||
|                 onValueChanges: (values, valid, isBuilding) { | ||||
|                   if (!isBuilding) { | ||||
|                     setState(() { | ||||
| @@ -369,9 +332,9 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|               ], | ||||
|             ), | ||||
|           ], | ||||
|                         ) | ||||
|                       else | ||||
|                         Expanded( | ||||
|         ); | ||||
|  | ||||
|     Widget getSourcesListWidget() => Expanded( | ||||
|             child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                 mainAxisAlignment: MainAxisAlignment.center, | ||||
| @@ -389,10 +352,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                   .map((e) => GestureDetector( | ||||
|                       onTap: e.host != null | ||||
|                           ? () { | ||||
|                                               launchUrlString( | ||||
|                                                   'https://${e.host}', | ||||
|                                                   mode: LaunchMode | ||||
|                                                       .externalApplication); | ||||
|                               launchUrlString('https://${e.host}', | ||||
|                                   mode: LaunchMode.externalApplication); | ||||
|                             } | ||||
|                           : null, | ||||
|                       child: Text( | ||||
| @@ -404,7 +365,28 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                             fontStyle: FontStyle.italic), | ||||
|                       ))) | ||||
|                   .toList() | ||||
|                             ])), | ||||
|             ])); | ||||
|  | ||||
|     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: [ | ||||
|                       getUrlInputRow(), | ||||
|                       if (shouldShowSearchBar()) | ||||
|                         const SizedBox( | ||||
|                           height: 16, | ||||
|                         ), | ||||
|                       if (shouldShowSearchBar()) getSearchBarRow(), | ||||
|                       if (pickedSource != null) | ||||
|                         getAdditionalOptsCol() | ||||
|                       else | ||||
|                         getSourcesListWidget(), | ||||
|                       const SizedBox( | ||||
|                         height: 8, | ||||
|                       ), | ||||
|   | ||||
| @@ -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,16 +35,22 @@ class _AppPageState extends State<AppPage> { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     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( | ||||
|     bool isVersionDetectionStandard = | ||||
|         app?.app.additionalSettings['versionDetection'] == | ||||
|             'standardVersionDetection'; | ||||
|  | ||||
|     getInfoColumn() => Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: [ | ||||
| @@ -66,7 +73,8 @@ class _AppPageState extends State<AppPage> { | ||||
|               height: 32, | ||||
|             ), | ||||
|             Text( | ||||
|           tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]), | ||||
|               tr('latestVersionX', | ||||
|                   args: [app?.app.latestVersion ?? tr('unknown')]), | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.bodyLarge, | ||||
|             ), | ||||
| @@ -79,6 +87,19 @@ class _AppPageState extends State<AppPage> { | ||||
|               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, | ||||
|             ), | ||||
| @@ -96,8 +117,9 @@ class _AppPageState extends State<AppPage> { | ||||
|             ), | ||||
|             CategoryEditorSelector( | ||||
|                 alignment: WrapAlignment.center, | ||||
|             preselected: | ||||
|                 app?.app.categories != null ? app!.app.categories.toSet() : {}, | ||||
|                 preselected: app?.app.categories != null | ||||
|                     ? app!.app.categories.toSet() | ||||
|                     : {}, | ||||
|                 onSelected: (categories) { | ||||
|                   if (app != null) { | ||||
|                     app.app.categories = categories; | ||||
| @@ -107,7 +129,7 @@ class _AppPageState extends State<AppPage> { | ||||
|           ], | ||||
|         ); | ||||
|  | ||||
|     var fullInfoColumn = Column( | ||||
|     getFullInfoColumn() => Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: [ | ||||
| @@ -152,51 +174,164 @@ class _AppPageState extends State<AppPage> { | ||||
|             const SizedBox( | ||||
|               height: 32, | ||||
|             ), | ||||
|         infoColumn, | ||||
|             getInfoColumn(), | ||||
|             const SizedBox(height: 150) | ||||
|           ], | ||||
|         ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: settingsProvider.showAppWebpage ? AppBar() : null, | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|       body: RefreshIndicator( | ||||
|           child: settingsProvider.showAppWebpage | ||||
|               ? app != null | ||||
|     getAppWebView() => app != null | ||||
|         ? WebViewWidget( | ||||
|             controller: WebViewController() | ||||
|               ..setJavaScriptMode(JavaScriptMode.unrestricted) | ||||
|                         ..setBackgroundColor( | ||||
|                             Theme.of(context).colorScheme.background) | ||||
|               ..setBackgroundColor(Theme.of(context).colorScheme.background) | ||||
|               ..setJavaScriptMode(JavaScriptMode.unrestricted) | ||||
|               ..setNavigationDelegate( | ||||
|                 NavigationDelegate( | ||||
|                   onWebResourceError: (WebResourceError error) { | ||||
|                     if (error.isForMainFrame == true) { | ||||
|                       showError( | ||||
|                                     ObtainiumError(error.description, | ||||
|                                         unexpected: true), | ||||
|                           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); | ||||
|         : 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]); | ||||
|                       } | ||||
|           }), | ||||
|       bottomSheet: Padding( | ||||
|           padding: EdgeInsets.fromLTRB( | ||||
|               0, 0, 0, MediaQuery.of(context).padding.bottom), | ||||
|                       Navigator.of(context).pop(); | ||||
|                     }, | ||||
|                     child: Text(tr('yesMarkUpdated'))) | ||||
|               ], | ||||
|             ); | ||||
|           }); | ||||
|     } | ||||
|  | ||||
|     showAdditionalOptionsDialog() async { | ||||
|       return await showDialog<Map<String, dynamic>?>( | ||||
|           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 && | ||||
|                       app?.app.additionalSettings['versionDetection'] != | ||||
|                           'releaseDateAsVersion' && | ||||
|                       !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<String, dynamic>? values) { | ||||
|       if (app != null && values != null) { | ||||
|         Map<String, dynamic> 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: [ | ||||
| @@ -205,129 +340,25 @@ class _AppPageState extends State<AppPage> { | ||||
|                 child: Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                     children: [ | ||||
|                         if (app?.app.additionalSettings['versionDetection'] != | ||||
|                                 'standardVersionDetection' && | ||||
|                             !trackOnly && | ||||
|                             app?.app.installedVersion != null && | ||||
|                             app?.app.installedVersion != app?.app.latestVersion) | ||||
|                       if (app?.app.installedVersion != null && | ||||
|                           app?.app.installedVersion != app?.app.latestVersion && | ||||
|                           !isVersionDetectionStandard && | ||||
|                           !trackOnly) | ||||
|                         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'))) | ||||
|                                               ], | ||||
|                                             ); | ||||
|                                           }); | ||||
|                                     }, | ||||
|                                 : showMarkUpdatedDialog, | ||||
|                             tooltip: tr('markUpdated'), | ||||
|                             icon: const Icon(Icons.done)), | ||||
|                       if (source != null && | ||||
|                             source | ||||
|                                 .combinedAppSpecificSettingFormItems.isNotEmpty) | ||||
|                           source.combinedAppSpecificSettingFormItems.isNotEmpty) | ||||
|                         IconButton( | ||||
|                             onPressed: app?.downloadProgress != null | ||||
|                                 ? null | ||||
|                                   : () { | ||||
|                                       showDialog<Map<String, dynamic>?>( | ||||
|                                           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<String, dynamic> | ||||
|                                               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); | ||||
|                                           }); | ||||
|                                         } | ||||
|                                       }); | ||||
|                                 : () async { | ||||
|                                     var values = | ||||
|                                         await showAdditionalOptionsDialog(); | ||||
|                                     handleAdditionalOptionChanges(values); | ||||
|                                   }, | ||||
|                             tooltip: tr('additionalOptions'), | ||||
|                             icon: const Icon(Icons.edit)), | ||||
| @@ -347,7 +378,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                                   builder: (BuildContext ctx) { | ||||
|                                     return AlertDialog( | ||||
|                                       scrollable: true, | ||||
|                                         content: infoColumn, | ||||
|                                       content: getInfoColumn(), | ||||
|                                       title: Text( | ||||
|                                           '${app.app.name} ${tr('byX', args: [ | ||||
|                                             app.app.author | ||||
| @@ -365,47 +396,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                             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')))), | ||||
|                       Expanded(child: getInstallOrUpdateButton()), | ||||
|                       const SizedBox(width: 16.0), | ||||
|                       Expanded( | ||||
|                           child: TextButton( | ||||
| @@ -433,7 +424,25 @@ class _AppPageState extends State<AppPage> { | ||||
|                   child: LinearProgressIndicator( | ||||
|                       value: app!.downloadProgress! / 100)) | ||||
|           ], | ||||
|           )), | ||||
|     ); | ||||
|         )); | ||||
|  | ||||
|     return Scaffold( | ||||
|         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()); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1245
									
								
								lib/pages/apps.dart
									
									
									
									
									
								
							
							
						
						
									
										1245
									
								
								lib/pages/apps.dart
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -30,6 +30,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     var appsProvider = context.read<AppsProvider>(); | ||||
|     var settingsProvider = context.read<SettingsProvider>(); | ||||
|  | ||||
|     var outlineButtonStyle = ButtonStyle( | ||||
|       shape: MaterialStateProperty.all( | ||||
|         StadiumBorder( | ||||
| @@ -101,6 +102,193 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     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<Map<String, dynamic>?>( | ||||
|             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; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     runMassSourceImport(MassAppUrlSource source) { | ||||
|       () async { | ||||
|         var values = await showDialog<Map<String, dynamic>?>( | ||||
|             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<String>?>( | ||||
|                   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: <Widget>[ | ||||
| @@ -120,18 +308,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                   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<ImportExportPage> { | ||||
|                                   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<ImportExportPage> { | ||||
|                               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<ImportExportPage> { | ||||
|                                         onPressed: importInProgress | ||||
|                                             ? null | ||||
|                                             : () { | ||||
|                                                 () async { | ||||
|                                                   var values = await showDialog< | ||||
|                                                           Map<String, | ||||
|                                                               dynamic>?>( | ||||
|                                                       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<ImportExportPage> { | ||||
|                                         onPressed: importInProgress | ||||
|                                             ? null | ||||
|                                             : () { | ||||
|                                                 () async { | ||||
|                                                   var values = await showDialog< | ||||
|                                                           Map<String, | ||||
|                                                               dynamic>?>( | ||||
|                                                       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<String>?>( | ||||
|                                                             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]))) | ||||
|   | ||||
| @@ -262,6 +262,18 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Text(tr('groupByCategory')), | ||||
|                                 Switch( | ||||
|                                     value: settingsProvider.groupByCategory, | ||||
|                                     onChanged: (value) { | ||||
|                                       settingsProvider.groupByCategory = value; | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             const Divider( | ||||
|                               height: 16, | ||||
|                             ), | ||||
|   | ||||
| @@ -73,6 +73,18 @@ List<String> generateStandardVersionRegExStrings() { | ||||
| List<String> standardVersionRegExStrings = | ||||
|     generateStandardVersionRegExStrings(); | ||||
|  | ||||
| Set<String> findStandardFormatsForVersion(String version, bool strict) { | ||||
|   // If !strict, even a substring match is valid | ||||
|   Set<String> 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<String, AppInMemory> apps = {}; | ||||
| @@ -472,55 +484,110 @@ 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?.app.additionalSettings['versionDetection'] != | ||||
|             'releaseDateAsVersion' && | ||||
|         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 && | ||||
|         app.additionalSettings['versionDetection'] == | ||||
|             'standardVersionDetection' && | ||||
|         !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 | ||||
|   MapEntry<bool, String>? reconcileVersionDifferences( | ||||
|       String templateVersion, String comparisonVersion) { | ||||
|     // Returns null if the versions don't share a common standard format | ||||
|     // Returns <true, comparisonVersion> if they share a common format and are equal | ||||
|     // Returns <false, templateVersion> 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 = | ||||
|         templateVersionFormats.intersection(comparisonVersionFormats); | ||||
|     if (commonStandardFormats.isEmpty) { | ||||
|       return null; | ||||
|     } | ||||
|     for (String pattern in commonStandardFormats) { | ||||
|       if (doStringsMatchUnderRegEx( | ||||
|           pattern, comparisonVersion, templateVersion)) { | ||||
|         return MapEntry(true, comparisonVersion); | ||||
|       } | ||||
|     } | ||||
|     return MapEntry(false, templateVersion); | ||||
|   } | ||||
|  | ||||
|     bool doStringsMatchUnderRegEx( | ||||
|         String pattern, String value1, String value2) { | ||||
|   bool doStringsMatchUnderRegEx(String pattern, String value1, String value2) { | ||||
|     var r = RegExp(pattern); | ||||
|     var m1 = r.firstMatch(value1); | ||||
|     var m2 = r.firstMatch(value2); | ||||
| @@ -530,38 +597,6 @@ class AppsProvider with ChangeNotifier { | ||||
|         : false; | ||||
|   } | ||||
|  | ||||
|     Set<String> findStandardFormatsForVersion(String version, bool strict) { | ||||
|       Set<String> 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); | ||||
|     var commonStandardFormats = | ||||
|         realStandardVersionFormats.intersection(internalStandardVersionFormats); | ||||
|     if (commonStandardFormats.isEmpty) { | ||||
|       return null; // Incompatible; no "enhanced detection" | ||||
|     } | ||||
|     for (String pattern in commonStandardFormats) { | ||||
|       if (doStringsMatchUnderRegEx(pattern, internalVersion, realVersion)) { | ||||
|         return matchMode | ||||
|             ? internalVersion | ||||
|             : null; // Enhanced detection says no change | ||||
|       } | ||||
|     } | ||||
|     return matchMode | ||||
|         ? null | ||||
|         : realVersion; // Enhanced detection says something changed | ||||
|   } | ||||
|  | ||||
|   Future<void> loadApps() async { | ||||
|     while (loadingApps) { | ||||
|       await Future.delayed(const Duration(microseconds: 1)); | ||||
|   | ||||
| @@ -139,6 +139,15 @@ class SettingsProvider with ChangeNotifier { | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get groupByCategory { | ||||
|     return prefs?.getBool('groupByCategory') ?? false; | ||||
|   } | ||||
|  | ||||
|   set groupByCategory(bool show) { | ||||
|     prefs?.setBool('groupByCategory', show); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   String? getSettingString(String settingId) { | ||||
|     return prefs?.getString(settingId); | ||||
|   } | ||||
|   | ||||
| @@ -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,7 +110,6 @@ 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'); | ||||
| @@ -122,6 +120,7 @@ class App { | ||||
|       if (additionalSettings['releaseDateAsVersion'] != null) { | ||||
|         additionalSettings.remove('releaseDateAsVersion'); | ||||
|       } | ||||
|     } | ||||
|     // Ensure additionalSettings are correctly typed | ||||
|     for (var item in formItems) { | ||||
|       if (additionalSettings[item.key] != null) { | ||||
|   | ||||
							
								
								
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								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: | ||||
|   | ||||
| @@ -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.17+139 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user