mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 21:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			v0.9.9-bet
			...
			v0.9.11-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3b449d0982 | ||
|  | 1863f55372 | ||
|  | 0c4b8ac79d | ||
|  | e287087753 | ||
|  | 82bcc46d42 | ||
|  | 1f26188ec6 | ||
|  | 794c3e1a81 | ||
|  | 16369b4adf | ||
|  | 8f16f745be | ||
|  | 8ddeb3d776 | ||
|  | 21cf9c98d9 | ||
|  | 358f910d19 | ||
|  | 7a3d74bd05 | 
| @@ -51,4 +51,7 @@ | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
|     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||
|         android:maxSdkVersion="28"/> | ||||
| </manifest> | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.1 KiB | 
| @@ -209,6 +209,8 @@ | ||||
|     "addCategory": "Kategorie hinzufügen", | ||||
|     "label": "Bezeichnung", | ||||
|     "language": "Sprache", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", | ||||
|         "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" | ||||
|   | ||||
| @@ -209,6 +209,8 @@ | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Too many requests (rate limited) - try again in {} minute", | ||||
|         "other": "Too many requests (rate limited) - try again in {} minutes" | ||||
|   | ||||
| @@ -207,6 +207,9 @@ | ||||
|     "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.", | ||||
|     "addCategory": "Új kategória", | ||||
|     "label": "Címke", | ||||
|     "language": "Language", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva", | ||||
|         "other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva" | ||||
|   | ||||
| @@ -209,6 +209,8 @@ | ||||
|     "addCategory": "Aggiungi categoria", | ||||
|     "label": "Etichetta", | ||||
|     "language": "Lingua", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", | ||||
|         "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" | ||||
|   | ||||
| @@ -209,6 +209,8 @@ | ||||
|     "addCategory": "カテゴリを追加", | ||||
|     "label": "ラベル", | ||||
|     "language": "言語", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||
|   | ||||
| @@ -209,6 +209,8 @@ | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", | ||||
|         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" | ||||
|   | ||||
| @@ -10,13 +10,15 @@ class GeneratedFormModal extends StatefulWidget { | ||||
|       required this.items, | ||||
|       this.initValid = false, | ||||
|       this.message = '', | ||||
|       this.additionalWidgets = const []}); | ||||
|       this.additionalWidgets = const [], | ||||
|       this.singleNullReturnButton}); | ||||
|  | ||||
|   final String title; | ||||
|   final String message; | ||||
|   final List<List<GeneratedFormItem>> items; | ||||
|   final bool initValid; | ||||
|   final List<Widget> additionalWidgets; | ||||
|   final String? singleNullReturnButton; | ||||
|  | ||||
|   @override | ||||
|   State<GeneratedFormModal> createState() => _GeneratedFormModalState(); | ||||
| @@ -64,17 +66,21 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> { | ||||
|             onPressed: () { | ||||
|               Navigator.of(context).pop(null); | ||||
|             }, | ||||
|             child: Text(tr('cancel'))), | ||||
|         TextButton( | ||||
|             onPressed: !valid | ||||
|                 ? null | ||||
|                 : () { | ||||
|                     if (valid) { | ||||
|                       HapticFeedback.selectionClick(); | ||||
|                       Navigator.of(context).pop(values); | ||||
|                     } | ||||
|                   }, | ||||
|             child: Text(tr('continue'))) | ||||
|             child: Text(widget.singleNullReturnButton == null | ||||
|                 ? tr('cancel') | ||||
|                 : widget.singleNullReturnButton!)), | ||||
|         widget.singleNullReturnButton == null | ||||
|             ? TextButton( | ||||
|                 onPressed: !valid | ||||
|                     ? null | ||||
|                     : () { | ||||
|                         if (valid) { | ||||
|                           HapticFeedback.selectionClick(); | ||||
|                           Navigator.of(context).pop(values); | ||||
|                         } | ||||
|                       }, | ||||
|                 child: Text(tr('continue'))) | ||||
|             : const SizedBox.shrink() | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -13,13 +13,10 @@ class ObtainiumError { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class RateLimitError { | ||||
| class RateLimitError extends ObtainiumError { | ||||
|   late int remainingMinutes; | ||||
|   RateLimitError(this.remainingMinutes); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       plural('tooManyRequestsTryAgainInMinutes', remainingMinutes); | ||||
|   RateLimitError(this.remainingMinutes) | ||||
|       : super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes)); | ||||
| } | ||||
|  | ||||
| class InvalidURLError extends ObtainiumError { | ||||
|   | ||||
| @@ -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.9.9'; | ||||
| const String currentVersion = '0.9.11'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|   AppSource? pickedSource; | ||||
|   Map<String, dynamic> additionalSettings = {}; | ||||
|   bool additionalSettingsValid = true; | ||||
|   String? category; | ||||
|   List<String> pickedCategories = []; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -127,9 +127,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           if (app.additionalSettings['trackOnly'] == true) { | ||||
|             app.installedVersion = app.latestVersion; | ||||
|           } | ||||
|           if (category != null) { | ||||
|             app.category = category; | ||||
|           } | ||||
|           app.categories = pickedCategories; | ||||
|           await appsProvider.saveApps([app]); | ||||
|  | ||||
|           return app; | ||||
| @@ -290,7 +288,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                           if (selectedUrls != null && | ||||
|                                               selectedUrls.isNotEmpty) { | ||||
|                                             changeUserInput( | ||||
|                                                 selectedUrls[0], true, true); | ||||
|                                                 selectedUrls[0], true, false); | ||||
|                                             addApp(resetUserInputAfter: true); | ||||
|                                           } | ||||
|                                         }).catchError((e) { | ||||
| @@ -334,11 +332,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                 ), | ||||
|                                 CategoryEditorSelector( | ||||
|                                     alignment: WrapAlignment.start, | ||||
|                                     singleSelect: true, | ||||
|                                     onSelected: (categories) { | ||||
|                                       category = categories.isEmpty | ||||
|                                           ? null | ||||
|                                           : categories.first; | ||||
|                                       pickedCategories = categories; | ||||
|                                     }), | ||||
|                               ], | ||||
|                             ), | ||||
|   | ||||
| @@ -42,6 +42,106 @@ class _AppPageState extends State<AppPage> { | ||||
|       getUpdate(app.app.id); | ||||
|     } | ||||
|     var trackOnly = app?.app.additionalSettings['trackOnly'] == true; | ||||
|  | ||||
|     var infoColumn = Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
|         GestureDetector( | ||||
|             onTap: () { | ||||
|               if (app?.app.url != null) { | ||||
|                 launchUrlString(app?.app.url ?? '', | ||||
|                     mode: LaunchMode.externalApplication); | ||||
|               } | ||||
|             }, | ||||
|             child: Text( | ||||
|               app?.app.url ?? '', | ||||
|               textAlign: TextAlign.center, | ||||
|               style: const TextStyle( | ||||
|                   decoration: TextDecoration.underline, | ||||
|                   fontStyle: FontStyle.italic, | ||||
|                   fontSize: 12), | ||||
|             )), | ||||
|         const SizedBox( | ||||
|           height: 32, | ||||
|         ), | ||||
|         Text( | ||||
|           tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]), | ||||
|           textAlign: TextAlign.center, | ||||
|           style: Theme.of(context).textTheme.bodyLarge, | ||||
|         ), | ||||
|         Text( | ||||
|           '${tr('installedVersionX', args: [ | ||||
|                 app?.app.installedVersion ?? tr('none') | ||||
|               ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ | ||||
|                   tr('app') | ||||
|                 ])}' : ''}', | ||||
|           textAlign: TextAlign.center, | ||||
|           style: Theme.of(context).textTheme.bodyLarge, | ||||
|         ), | ||||
|         const SizedBox( | ||||
|           height: 32, | ||||
|         ), | ||||
|         Text( | ||||
|           tr('lastUpdateCheckX', args: [ | ||||
|             app?.app.lastUpdateCheck == null | ||||
|                 ? tr('never') | ||||
|                 : '\n${app?.app.lastUpdateCheck?.toLocal()}' | ||||
|           ]), | ||||
|           textAlign: TextAlign.center, | ||||
|           style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), | ||||
|         ), | ||||
|         const SizedBox( | ||||
|           height: 48, | ||||
|         ), | ||||
|         CategoryEditorSelector( | ||||
|             alignment: WrapAlignment.center, | ||||
|             preselected: | ||||
|                 app?.app.categories != null ? app!.app.categories.toSet() : {}, | ||||
|             onSelected: (categories) { | ||||
|               if (app != null) { | ||||
|                 app.app.categories = categories; | ||||
|                 appsProvider.saveApps([app.app]); | ||||
|               } | ||||
|             }), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     var fullInfoColumn = Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
|         const SizedBox(height: 150), | ||||
|         app?.installedInfo != null | ||||
|             ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ | ||||
|                 Image.memory( | ||||
|                   app!.installedInfo!.icon!, | ||||
|                   height: 150, | ||||
|                   gaplessPlayback: true, | ||||
|                 ) | ||||
|               ]) | ||||
|             : Container(), | ||||
|         const SizedBox( | ||||
|           height: 25, | ||||
|         ), | ||||
|         Text( | ||||
|           app?.installedInfo?.name ?? app?.app.name ?? tr('app'), | ||||
|           textAlign: TextAlign.center, | ||||
|           style: Theme.of(context).textTheme.displayLarge, | ||||
|         ), | ||||
|         Text( | ||||
|           tr('byX', args: [app?.app.author ?? tr('unknown')]), | ||||
|           textAlign: TextAlign.center, | ||||
|           style: Theme.of(context).textTheme.headlineMedium, | ||||
|         ), | ||||
|         const SizedBox( | ||||
|           height: 32, | ||||
|         ), | ||||
|         infoColumn, | ||||
|         const SizedBox(height: 150) | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: settingsProvider.showAppWebpage ? AppBar() : null, | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
| @@ -71,106 +171,7 @@ class _AppPageState extends State<AppPage> { | ||||
|               : CustomScrollView( | ||||
|                   slivers: [ | ||||
|                     SliverToBoxAdapter( | ||||
|                         child: Column( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                       children: [ | ||||
|                         const SizedBox(height: 150), | ||||
|                         app?.installedInfo != null | ||||
|                             ? Row( | ||||
|                                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                     Image.memory( | ||||
|                                       app!.installedInfo!.icon!, | ||||
|                                       height: 150, | ||||
|                                       gaplessPlayback: true, | ||||
|                                     ) | ||||
|                                   ]) | ||||
|                             : Container(), | ||||
|                         const SizedBox( | ||||
|                           height: 25, | ||||
|                         ), | ||||
|                         Text( | ||||
|                           app?.installedInfo?.name ?? | ||||
|                               app?.app.name ?? | ||||
|                               tr('app'), | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: Theme.of(context).textTheme.displayLarge, | ||||
|                         ), | ||||
|                         Text( | ||||
|                           tr('byX', args: [app?.app.author ?? tr('unknown')]), | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: Theme.of(context).textTheme.headlineMedium, | ||||
|                         ), | ||||
|                         const SizedBox( | ||||
|                           height: 32, | ||||
|                         ), | ||||
|                         GestureDetector( | ||||
|                             onTap: () { | ||||
|                               if (app?.app.url != null) { | ||||
|                                 launchUrlString(app?.app.url ?? '', | ||||
|                                     mode: LaunchMode.externalApplication); | ||||
|                               } | ||||
|                             }, | ||||
|                             child: Text( | ||||
|                               app?.app.url ?? '', | ||||
|                               textAlign: TextAlign.center, | ||||
|                               style: const TextStyle( | ||||
|                                   decoration: TextDecoration.underline, | ||||
|                                   fontStyle: FontStyle.italic, | ||||
|                                   fontSize: 12), | ||||
|                             )), | ||||
|                         const SizedBox( | ||||
|                           height: 32, | ||||
|                         ), | ||||
|                         Text( | ||||
|                           tr('latestVersionX', | ||||
|                               args: [app?.app.latestVersion ?? tr('unknown')]), | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                         ), | ||||
|                         Text( | ||||
|                           '${tr('installedVersionX', args: [ | ||||
|                                 app?.app.installedVersion ?? tr('none') | ||||
|                               ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ | ||||
|                                   tr('app') | ||||
|                                 ])}' : ''}', | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                         ), | ||||
|                         const SizedBox( | ||||
|                           height: 32, | ||||
|                         ), | ||||
|                         Text( | ||||
|                           tr('lastUpdateCheckX', args: [ | ||||
|                             app?.app.lastUpdateCheck == null | ||||
|                                 ? tr('never') | ||||
|                                 : '\n${app?.app.lastUpdateCheck?.toLocal()}' | ||||
|                           ]), | ||||
|                           textAlign: TextAlign.center, | ||||
|                           style: const TextStyle( | ||||
|                               fontStyle: FontStyle.italic, fontSize: 12), | ||||
|                         ), | ||||
|                         const SizedBox( | ||||
|                           height: 48, | ||||
|                         ), | ||||
|                         CategoryEditorSelector( | ||||
|                             alignment: WrapAlignment.center, | ||||
|                             singleSelect: true, | ||||
|                             preselected: app?.app.category != null | ||||
|                                 ? {app!.app.category!} | ||||
|                                 : {}, | ||||
|                             onSelected: (categories) { | ||||
|                               if (app != null) { | ||||
|                                 app.app.category = categories.isNotEmpty | ||||
|                                     ? categories[0] | ||||
|                                     : null; | ||||
|                                 appsProvider.saveApps([app.app]); | ||||
|                               } | ||||
|                             }), | ||||
|                         const SizedBox(height: 150) | ||||
|                       ], | ||||
|                     )), | ||||
|                         child: Column(children: [fullInfoColumn])), | ||||
|                   ], | ||||
|                 ), | ||||
|           onRefresh: () async { | ||||
| @@ -289,6 +290,31 @@ class _AppPageState extends State<AppPage> { | ||||
|                                     }, | ||||
|                               tooltip: tr('additionalOptions'), | ||||
|                               icon: const Icon(Icons.settings)), | ||||
|                         if (app != null && settingsProvider.showAppWebpage) | ||||
|                           IconButton( | ||||
|                               onPressed: () { | ||||
|                                 showDialog( | ||||
|                                     context: context, | ||||
|                                     builder: (BuildContext ctx) { | ||||
|                                       return AlertDialog( | ||||
|                                         scrollable: true, | ||||
|                                         content: infoColumn, | ||||
|                                         title: Text( | ||||
|                                             '${app.app.name} ${tr('byX', args: [ | ||||
|                                               app.app.author | ||||
|                                             ])}'), | ||||
|                                         actions: [ | ||||
|                                           TextButton( | ||||
|                                               onPressed: () { | ||||
|                                                 Navigator.of(context).pop(); | ||||
|                                               }, | ||||
|                                               child: Text(tr('continue'))) | ||||
|                                         ], | ||||
|                                       ); | ||||
|                                     }); | ||||
|                               }, | ||||
|                               icon: const Icon(Icons.more_horiz), | ||||
|                               tooltip: tr('more')), | ||||
|                         const SizedBox(width: 16.0), | ||||
|                         Expanded( | ||||
|                             child: ElevatedButton( | ||||
|   | ||||
| @@ -55,7 +55,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|     var appsProvider = context.watch<AppsProvider>(); | ||||
|     var settingsProvider = context.watch<SettingsProvider>(); | ||||
|     var sortedApps = appsProvider.apps.values.toList(); | ||||
|     var currentFilterIsUpdatesOnly = filter.isIdenticalTo(updatesOnlyFilter); | ||||
|     var currentFilterIsUpdatesOnly = | ||||
|         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); | ||||
|  | ||||
|     selectedApps = selectedApps | ||||
|         .where((element) => sortedApps.map((e) => e.app).contains(element)) | ||||
| @@ -102,7 +103,9 @@ class AppsPageState extends State<AppsPage> { | ||||
|         } | ||||
|       } | ||||
|       if (filter.categoryFilter.isNotEmpty && | ||||
|           !filter.categoryFilter.contains(app.app.category)) { | ||||
|           filter.categoryFilter | ||||
|               .intersection(app.app.categories.toSet()) | ||||
|               .isEmpty) { | ||||
|         return false; | ||||
|       } | ||||
|       return true; | ||||
| @@ -224,14 +227,21 @@ class AppsPageState extends State<AppsPage> { | ||||
|               String? changesUrl = SourceProvider() | ||||
|                   .getSource(sortedApps[index].app.url) | ||||
|                   .changeLogPageFromStandardUrl(sortedApps[index].app.url); | ||||
|               var transparent = const Color.fromARGB(0, 0, 0, 0).value; | ||||
|               return Container( | ||||
|                   decoration: BoxDecoration( | ||||
|                       border: Border.symmetric( | ||||
|                           vertical: BorderSide( | ||||
|                               width: 4, | ||||
|                               color: Color(settingsProvider.categories[ | ||||
|                                       sortedApps[index].app.category] ?? | ||||
|                                   const Color.fromARGB(0, 0, 0, 0).value)))), | ||||
|                               color: Color( | ||||
|                                   sortedApps[index].app.categories.isNotEmpty | ||||
|                                       ? settingsProvider.categories[ | ||||
|                                               sortedApps[index] | ||||
|                                                   .app | ||||
|                                                   .categories | ||||
|                                                   .first] ?? | ||||
|                                           transparent | ||||
|                                       : transparent)))), | ||||
|                   child: ListTile( | ||||
|                     tileColor: sortedApps[index].app.pinned | ||||
|                         ? Colors.grey.withOpacity(0.1) | ||||
| @@ -339,6 +349,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|           children: [ | ||||
|             selectedApps.isEmpty | ||||
|                 ? IconButton( | ||||
|                     visualDensity: VisualDensity.compact, | ||||
|                     onPressed: () { | ||||
|                       selectThese(sortedApps.map((e) => e.app).toList()); | ||||
|                     }, | ||||
| @@ -348,6 +359,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                     ), | ||||
|                     tooltip: tr('selectAll')) | ||||
|                 : TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                     onPressed: () { | ||||
|                       selectedApps.isEmpty | ||||
|                           ? selectThese(sortedApps.map((e) => e.app).toList()) | ||||
| @@ -492,6 +505,73 @@ class AppsPageState extends State<AppsPage> { | ||||
|                     icon: const Icon( | ||||
|                       Icons.file_download_outlined, | ||||
|                     )), | ||||
|                 selectedApps.isEmpty | ||||
|                     ? const SizedBox() | ||||
|                     : IconButton( | ||||
|                         visualDensity: VisualDensity.compact, | ||||
|                         onPressed: () async { | ||||
|                           try { | ||||
|                             Set<String>? preselected; | ||||
|                             var showPrompt = false; | ||||
|                             for (var element in selectedApps) { | ||||
|                               var currentCats = element.categories.toSet(); | ||||
|                               if (preselected == null) { | ||||
|                                 preselected = currentCats; | ||||
|                               } else { | ||||
|                                 if (!settingsProvider.setEqual( | ||||
|                                     currentCats, preselected)) { | ||||
|                                   showPrompt = true; | ||||
|                                   break; | ||||
|                                 } | ||||
|                               } | ||||
|                             } | ||||
|                             var cont = true; | ||||
|                             if (showPrompt) { | ||||
|                               cont = await showDialog<Map<String, dynamic>?>( | ||||
|                                       context: context, | ||||
|                                       builder: (BuildContext ctx) { | ||||
|                                         return GeneratedFormModal( | ||||
|                                           title: tr('categorize'), | ||||
|                                           items: const [], | ||||
|                                           initValid: true, | ||||
|                                           message: | ||||
|                                               tr('selectedCategorizeWarning'), | ||||
|                                         ); | ||||
|                                       }) != | ||||
|                                   null; | ||||
|                             } | ||||
|                             if (cont) { | ||||
|                               await showDialog<Map<String, dynamic>?>( | ||||
|                                   context: context, | ||||
|                                   builder: (BuildContext ctx) { | ||||
|                                     return GeneratedFormModal( | ||||
|                                       title: tr('categorize'), | ||||
|                                       items: const [], | ||||
|                                       initValid: true, | ||||
|                                       singleNullReturnButton: tr('continue'), | ||||
|                                       additionalWidgets: [ | ||||
|                                         CategoryEditorSelector( | ||||
|                                           preselected: preselected ?? {}, | ||||
|                                           showLabelWhenNotEmpty: false, | ||||
|                                           onSelected: (categories) { | ||||
|                                             appsProvider | ||||
|                                                 .saveApps(selectedApps.map((e) { | ||||
|                                               e.categories = categories; | ||||
|                                               return e; | ||||
|                                             }).toList()); | ||||
|                                           }, | ||||
|                                         ) | ||||
|                                       ], | ||||
|                                     ); | ||||
|                                   }); | ||||
|                             } | ||||
|                           } catch (err) { | ||||
|                             showError(err, context); | ||||
|                           } | ||||
|                         }, | ||||
|                         tooltip: tr('categorize'), | ||||
|                         icon: const Icon(Icons.category_outlined), | ||||
|                       ), | ||||
|                 selectedApps.isEmpty | ||||
|                     ? const SizedBox() | ||||
|                     : IconButton( | ||||
| @@ -688,12 +768,15 @@ class AppsPageState extends State<AppsPage> { | ||||
|             appsProvider.apps.isEmpty | ||||
|                 ? const SizedBox() | ||||
|                 : TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                     label: Text( | ||||
|                       filter.isIdenticalTo(neutralFilter) | ||||
|                       filter.isIdenticalTo(neutralFilter, settingsProvider) | ||||
|                           ? tr('filter') | ||||
|                           : tr('filterActive'), | ||||
|                       style: TextStyle( | ||||
|                           fontWeight: filter.isIdenticalTo(neutralFilter) | ||||
|                           fontWeight: filter.isIdenticalTo( | ||||
|                                   neutralFilter, settingsProvider) | ||||
|                               ? FontWeight.normal | ||||
|                               : FontWeight.bold), | ||||
|                     ), | ||||
| @@ -785,12 +868,10 @@ class AppsFilter { | ||||
|     includeNonInstalled = values['nonInstalledApps']; | ||||
|   } | ||||
|  | ||||
|   bool isIdenticalTo(AppsFilter other) => | ||||
|   bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) => | ||||
|       authorFilter.trim() == other.authorFilter.trim() && | ||||
|       nameFilter.trim() == other.nameFilter.trim() && | ||||
|       includeUptodate == other.includeUptodate && | ||||
|       includeNonInstalled == other.includeNonInstalled && | ||||
|       categoryFilter.length == other.categoryFilter.length && | ||||
|       categoryFilter.union(other.categoryFilter).length == | ||||
|           categoryFilter.length; | ||||
|       settingsProvider.setEqual(categoryFilter, other.categoryFilter); | ||||
| } | ||||
|   | ||||
| @@ -66,6 +66,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                             showError( | ||||
|                                                 tr('exportedTo', args: [path]), | ||||
|                                                 context); | ||||
|                                           }).catchError((e) { | ||||
|                                             showError(e, context); | ||||
|                                           }); | ||||
|                                         }, | ||||
|                                   child: Text(tr('obtainiumExport')))), | ||||
|   | ||||
| @@ -436,7 +436,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | ||||
|         items: [ | ||||
|           [ | ||||
|             GeneratedFormTagInput('categories', | ||||
|                 label: tr('category'), | ||||
|                 label: tr('categories'), | ||||
|                 emptyMessage: tr('noCategories'), | ||||
|                 defaultValue: storedValues, | ||||
|                 alignment: widget.alignment, | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import 'package:obtainium/providers/logs_provider.dart'; | ||||
| import 'package:obtainium/providers/notifications_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:package_archive_info/package_archive_info.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:flutter_fgbg/flutter_fgbg.dart'; | ||||
| @@ -706,6 +707,14 @@ class AppsProvider with ChangeNotifier { | ||||
|       exportDir = await getExternalStorageDirectory(); | ||||
|       path = exportDir!.path; | ||||
|     } | ||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) { | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         await Permission.storage.request(); | ||||
|       } | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         throw ObtainiumError(tr('storagePermissionDenied')); | ||||
|       } | ||||
|     } | ||||
|     File export = File( | ||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); | ||||
|     export.writeAsStringSync( | ||||
|   | ||||
| @@ -157,15 +157,6 @@ class SettingsProvider with ChangeNotifier { | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown( | ||||
|       'category', | ||||
|       label: tr('category'), | ||||
|       [ | ||||
|         MapEntry('', tr('noCategory')), | ||||
|         ...categories.entries.map((e) => MapEntry(e.key, e.key)).toList() | ||||
|       ], | ||||
|       defaultValue: initCategory); | ||||
|  | ||||
|   String? get forcedLocale { | ||||
|     var fl = prefs?.getString('forcedLocale'); | ||||
|     return supportedLocales | ||||
| @@ -185,4 +176,7 @@ class SettingsProvider with ChangeNotifier { | ||||
|     } | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool setEqual(Set<String> a, Set<String> b) => | ||||
|       a.length == b.length && a.union(b).length == a.length; | ||||
| } | ||||
|   | ||||
| @@ -48,7 +48,7 @@ class App { | ||||
|   late Map<String, dynamic> additionalSettings; | ||||
|   late DateTime? lastUpdateCheck; | ||||
|   bool pinned = false; | ||||
|   String? category; | ||||
|   List<String> categories; | ||||
|   App( | ||||
|       this.id, | ||||
|       this.url, | ||||
| @@ -61,7 +61,7 @@ class App { | ||||
|       this.additionalSettings, | ||||
|       this.lastUpdateCheck, | ||||
|       this.pinned, | ||||
|       {this.category}); | ||||
|       {this.categories = const []}); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -103,6 +103,12 @@ class App { | ||||
|             item.ensureType(additionalSettings[item.key]); | ||||
|       } | ||||
|     } | ||||
|     int preferredApkIndex = json['preferredApkIndex'] == null | ||||
|         ? 0 | ||||
|         : json['preferredApkIndex'] as int; | ||||
|     if (preferredApkIndex < 0) { | ||||
|       preferredApkIndex = 0; | ||||
|     } | ||||
|     return App( | ||||
|         json['id'] as String, | ||||
|         json['url'] as String, | ||||
| @@ -115,15 +121,19 @@ class App { | ||||
|         json['apkUrls'] == null | ||||
|             ? [] | ||||
|             : List<String>.from(jsonDecode(json['apkUrls'])), | ||||
|         json['preferredApkIndex'] == null | ||||
|             ? 0 | ||||
|             : json['preferredApkIndex'] as int, | ||||
|         preferredApkIndex, | ||||
|         additionalSettings, | ||||
|         json['lastUpdateCheck'] == null | ||||
|             ? null | ||||
|             : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), | ||||
|         json['pinned'] ?? false, | ||||
|         category: json['category']); | ||||
|         categories: json['categories'] != null | ||||
|             ? (json['categories'] as List<dynamic>) | ||||
|                 .map((e) => e.toString()) | ||||
|                 .toList() | ||||
|             : json['category'] != null | ||||
|                 ? [json['category'] as String] | ||||
|                 : []); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
| @@ -138,7 +148,7 @@ class App { | ||||
|         'additionalSettings': jsonEncode(additionalSettings), | ||||
|         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, | ||||
|         'pinned': pinned, | ||||
|         'category': category | ||||
|         'categories': categories | ||||
|       }; | ||||
| } | ||||
|  | ||||
| @@ -360,11 +370,11 @@ class SourceProvider { | ||||
|         currentApp?.installedVersion, | ||||
|         apkVersion, | ||||
|         apk.apkUrls, | ||||
|         apk.apkUrls.length - 1, | ||||
|         apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0, | ||||
|         additionalSettings, | ||||
|         DateTime.now(), | ||||
|         currentApp?.pinned ?? false, | ||||
|         category: currentApp?.category); | ||||
|         categories: currentApp?.categories ?? const []); | ||||
|   } | ||||
|  | ||||
|   // Returns errors in [results, errors] instead of throwing them | ||||
|   | ||||
| @@ -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.9.9+97 # When changing this, update the tag in main() accordingly | ||||
| version: 0.9.11+101 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user