mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-25 20:03:44 +02:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
			v0.11.16-b
			...
			v0.11.22-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | dea635fa6a | ||
|  | 682026ed0a | ||
|  | 210100da2b | ||
|  | d52660235b | ||
|  | e386b5ab8a | ||
|  | abf7be222d | ||
|  | 4c5b9304c0 | ||
|  | 4cfe6af044 | ||
|  | 3f0c4068dd | ||
|  | 7981ca29c5 | ||
|  | 187efa8fc5 | ||
|  | cd27ff7f2d | ||
|  | 6f6a25511b | ||
|  | 4e17bbcfd1 | 
| @@ -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,7 +1,6 @@ | ||||
| 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'; | ||||
|  | ||||
| @@ -29,24 +28,41 @@ class Mullvad extends AppSource { | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     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(); | ||||
|     Response res = await get(Uri.parse('$standardUrl/en/download/android')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var versions = parse(res.body) | ||||
|           .querySelectorAll('p') | ||||
|           .map((e) => e.innerHtml) | ||||
|           .where((p) => p.contains('Latest version: ')) | ||||
|           .map((e) { | ||||
|             var match = RegExp('[0-9]+(\\.[0-9]+)*').firstMatch(e); | ||||
|             if (match == null) { | ||||
|               return ''; | ||||
|             } else { | ||||
|               return e.substring(match.start, match.end); | ||||
|             } | ||||
|           }) | ||||
|           .where((element) => element.isNotEmpty) | ||||
|           .toList(); | ||||
|       if (versions.isEmpty) { | ||||
|         throw NoVersionError(); | ||||
|       } | ||||
|       String? changeLog; | ||||
|       try { | ||||
|         changeLog = (await GitHub().getLatestAPKDetails( | ||||
|                 'https://github.com/mullvad/mullvadvpn-app', | ||||
|                 {'fallbackToOlderReleases': true})) | ||||
|             .changeLog; | ||||
|       } catch (e) { | ||||
|         // Ignore | ||||
|       } | ||||
|       return APKDetails( | ||||
|           versions[0], | ||||
|           ['https://mullvad.net/download/app/apk/latest'], | ||||
|           AppNames(name, 'Mullvad-VPN'), | ||||
|           changeLog: changeLog); | ||||
|     } 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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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.16'; | ||||
| const String currentVersion = '0.11.22'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
|   | ||||
| @@ -56,6 +56,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|   Widget build(BuildContext context) { | ||||
|     var appsProvider = context.watch<AppsProvider>(); | ||||
|     var settingsProvider = context.watch<SettingsProvider>(); | ||||
|     var sourceProvider = SourceProvider(); | ||||
|     var listedApps = appsProvider.apps.values.toList(); | ||||
|     var currentFilterIsUpdatesOnly = | ||||
|         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); | ||||
| @@ -110,6 +111,11 @@ class AppsPageState extends State<AppsPage> { | ||||
|               .isEmpty) { | ||||
|         return false; | ||||
|       } | ||||
|       if (filter.sourceFilter.isNotEmpty && | ||||
|           sourceProvider.getSource(app.app.url).runtimeType.toString() != | ||||
|               filter.sourceFilter) { | ||||
|         return false; | ||||
|       } | ||||
|       return true; | ||||
|     }).toList(); | ||||
|  | ||||
| @@ -187,6 +193,25 @@ class AppsPageState extends State<AppsPage> { | ||||
|     } | ||||
|     listedApps = [...tempPinned, ...tempNotPinned]; | ||||
|  | ||||
|     List<String?> getListedCategories() { | ||||
|       var temp = listedApps | ||||
|           .map((e) => e.app.categories.isNotEmpty ? e.app.categories : [null]); | ||||
|       return temp.isNotEmpty | ||||
|           ? { | ||||
|               ...temp.reduce((v, e) => [...v, ...e]) | ||||
|             }.toList() | ||||
|           : []; | ||||
|     } | ||||
|  | ||||
|     var listedCategories = getListedCategories(); | ||||
|     listedCategories.sort((a, b) { | ||||
|       return a != null && b != null | ||||
|           ? a.compareTo(b) | ||||
|           : a == null | ||||
|               ? 1 | ||||
|               : -1; | ||||
|     }); | ||||
|  | ||||
|     showChangeLogDialog( | ||||
|         String? changesUrl, AppSource appSource, String changeLog, int index) { | ||||
|       showDialog( | ||||
| @@ -402,17 +427,38 @@ class AppsPageState extends State<AppsPage> { | ||||
|         ], | ||||
|       ); | ||||
|  | ||||
|       var transparent = const Color.fromARGB(0, 0, 0, 0).value; | ||||
|       var transparent = | ||||
|           Theme.of(context).colorScheme.background.withAlpha(0).value; | ||||
|       List<double> stops = [ | ||||
|         ...listedApps[index] | ||||
|             .app | ||||
|             .categories | ||||
|             .asMap() | ||||
|             .entries | ||||
|             .map((e) => | ||||
|                 ((e.key / (listedApps[index].app.categories.length - 1)))) | ||||
|             .toList(), | ||||
|         1 | ||||
|       ]; | ||||
|       if (stops.length == 2) { | ||||
|         stops[0] = 1; | ||||
|       } | ||||
|       return Container( | ||||
|           decoration: BoxDecoration( | ||||
|               border: Border.symmetric( | ||||
|                   vertical: BorderSide( | ||||
|                       width: 4, | ||||
|                       color: Color(listedApps[index].app.categories.isNotEmpty | ||||
|                           ? settingsProvider.categories[ | ||||
|                                   listedApps[index].app.categories.first] ?? | ||||
|                               transparent | ||||
|                           : transparent)))), | ||||
|               gradient: LinearGradient( | ||||
|                   stops: stops, | ||||
|                   begin: const Alignment(-1, 0), | ||||
|                   end: const Alignment(-0.97, 0), | ||||
|                   colors: [ | ||||
|                 ...listedApps[index] | ||||
|                     .app | ||||
|                     .categories | ||||
|                     .map((e) => | ||||
|                         Color(settingsProvider.categories[e] ?? transparent) | ||||
|                             .withAlpha(255)) | ||||
|                     .toList(), | ||||
|                 Color(transparent) | ||||
|               ])), | ||||
|           child: ListTile( | ||||
|             tileColor: listedApps[index].app.pinned | ||||
|                 ? Colors.grey.withOpacity(0.1) | ||||
| @@ -465,6 +511,28 @@ class AppsPageState extends State<AppsPage> { | ||||
|           )); | ||||
|     } | ||||
|  | ||||
|     getCategoryCollapsibleTile(int index) { | ||||
|       var tiles = listedApps | ||||
|           .asMap() | ||||
|           .entries | ||||
|           .where((e) => | ||||
|               e.value.app.categories.contains(listedCategories[index]) || | ||||
|               e.value.app.categories.isEmpty && listedCategories[index] == null) | ||||
|           .map((e) => getSingleAppHorizTile(e.key)) | ||||
|           .toList(); | ||||
|  | ||||
|       capFirstChar(String str) => str[0].toUpperCase() + str.substring(1); | ||||
|       return ExpansionTile( | ||||
|           initiallyExpanded: true, | ||||
|           title: Text( | ||||
|             capFirstChar(listedCategories[index] ?? tr('noCategory')), | ||||
|             style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|           ), | ||||
|           controlAffinity: ListTileControlAffinity.leading, | ||||
|           trailing: Text(tiles.length.toString()), | ||||
|           children: tiles); | ||||
|     } | ||||
|  | ||||
|     getSelectAllButton() { | ||||
|       return selectedApps.isEmpty | ||||
|           ? TextButton.icon( | ||||
| @@ -673,14 +741,12 @@ class AppsPageState extends State<AppsPage> { | ||||
|     } | ||||
|  | ||||
|     pinSelectedApps() { | ||||
|       () { | ||||
|         var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; | ||||
|         appsProvider.saveApps(selectedApps.map((e) { | ||||
|           e.pinned = pinStatus; | ||||
|           return e; | ||||
|         }).toList()); | ||||
|         Navigator.of(context).pop(); | ||||
|       }; | ||||
|       var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; | ||||
|       appsProvider.saveApps(selectedApps.map((e) { | ||||
|         e.pinned = pinStatus; | ||||
|         return e; | ||||
|       }).toList()); | ||||
|       Navigator.of(context).pop(); | ||||
|     } | ||||
|  | ||||
|     resetSelectedAppsInstallStatuses() { | ||||
| @@ -832,6 +898,19 @@ class AppsPageState extends State<AppsPage> { | ||||
|                   GeneratedFormSwitch('nonInstalledApps', | ||||
|                       label: tr('nonInstalledApps'), | ||||
|                       defaultValue: vals['nonInstalledApps']) | ||||
|                 ], | ||||
|                 [ | ||||
|                   GeneratedFormDropdown( | ||||
|                       'sourceFilter', | ||||
|                       label: tr('appSource'), | ||||
|                       defaultValue: filter.sourceFilter, | ||||
|                       [ | ||||
|                         MapEntry('', tr('none')), | ||||
|                         ...sourceProvider.sources | ||||
|                             .map((e) => | ||||
|                                 MapEntry(e.runtimeType.toString(), e.name)) | ||||
|                             .toList() | ||||
|                       ]) | ||||
|                 ] | ||||
|               ], | ||||
|               additionalWidgets: [ | ||||
| @@ -903,6 +982,22 @@ class AppsPageState extends State<AppsPage> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     getDisplayedList() { | ||||
|       return settingsProvider.groupByCategory && | ||||
|               !(listedCategories.isEmpty || | ||||
|                   (listedCategories.length == 1 && listedCategories[0] == null)) | ||||
|           ? SliverList( | ||||
|               delegate: | ||||
|                   SliverChildBuilderDelegate((BuildContext context, int index) { | ||||
|               return getCategoryCollapsibleTile(index); | ||||
|             }, childCount: listedCategories.length)) | ||||
|           : SliverList( | ||||
|               delegate: | ||||
|                   SliverChildBuilderDelegate((BuildContext context, int index) { | ||||
|               return getSingleAppHorizTile(index); | ||||
|             }, childCount: listedApps.length)); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|       body: RefreshIndicator( | ||||
| @@ -922,11 +1017,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|           child: CustomScrollView(slivers: <Widget>[ | ||||
|             CustomAppBar(title: tr('appsString')), | ||||
|             ...getLoadingWidgets(), | ||||
|             SliverList( | ||||
|                 delegate: SliverChildBuilderDelegate( | ||||
|                     (BuildContext context, int index) { | ||||
|               return getSingleAppHorizTile(index); | ||||
|             }, childCount: listedApps.length)) | ||||
|             getDisplayedList() | ||||
|           ])), | ||||
|       persistentFooterButtons: appsProvider.apps.isEmpty | ||||
|           ? null | ||||
| @@ -943,20 +1034,23 @@ class AppsFilter { | ||||
|   late bool includeUptodate; | ||||
|   late bool includeNonInstalled; | ||||
|   late Set<String> categoryFilter; | ||||
|   late String sourceFilter; | ||||
|  | ||||
|   AppsFilter( | ||||
|       {this.nameFilter = '', | ||||
|       this.authorFilter = '', | ||||
|       this.includeUptodate = true, | ||||
|       this.includeNonInstalled = true, | ||||
|       this.categoryFilter = const {}}); | ||||
|       this.categoryFilter = const {}, | ||||
|       this.sourceFilter = ''}); | ||||
|  | ||||
|   Map<String, dynamic> toFormValuesMap() { | ||||
|     return { | ||||
|       'appName': nameFilter, | ||||
|       'author': authorFilter, | ||||
|       'upToDateApps': includeUptodate, | ||||
|       'nonInstalledApps': includeNonInstalled | ||||
|       'nonInstalledApps': includeNonInstalled, | ||||
|       'sourceFilter': sourceFilter | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -965,6 +1059,7 @@ class AppsFilter { | ||||
|     authorFilter = values['author']!; | ||||
|     includeUptodate = values['upToDateApps']; | ||||
|     includeNonInstalled = values['nonInstalledApps']; | ||||
|     sourceFilter = values['sourceFilter']; | ||||
|   } | ||||
|  | ||||
|   bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) => | ||||
| @@ -972,5 +1067,6 @@ class AppsFilter { | ||||
|       nameFilter.trim() == other.nameFilter.trim() && | ||||
|       includeUptodate == other.includeUptodate && | ||||
|       includeNonInstalled == other.includeNonInstalled && | ||||
|       settingsProvider.setEqual(categoryFilter, other.categoryFilter); | ||||
|       settingsProvider.setEqual(categoryFilter, other.categoryFilter) && | ||||
|       sourceFilter.trim() == other.sourceFilter.trim(); | ||||
| } | ||||
|   | ||||
| @@ -133,7 +133,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                 } | ||||
|               } | ||||
|             }); | ||||
|             settingsProvider.categories = cats; | ||||
|             appsProvider.addMissingCategories(settingsProvider); | ||||
|             showError(tr('importedX', args: [plural('apps', value)]), context); | ||||
|           }); | ||||
|         } else { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/logs_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -262,6 +263,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, | ||||
|                             ), | ||||
| @@ -432,6 +445,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var settingsProvider = context.watch<SettingsProvider>(); | ||||
|     var appsProvider = context.watch<AppsProvider>(); | ||||
|     storedValues = settingsProvider.categories.map((key, value) => MapEntry( | ||||
|         key, | ||||
|         MapEntry(value, | ||||
| @@ -455,8 +469,9 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | ||||
|           if (!isBuilding) { | ||||
|             storedValues = | ||||
|                 values['categories'] as Map<String, MapEntry<int, bool>>; | ||||
|             settingsProvider.categories = | ||||
|                 storedValues.map((key, value) => MapEntry(key, value.key)); | ||||
|             settingsProvider.setCategories( | ||||
|                 storedValues.map((key, value) => MapEntry(key, value.key)), | ||||
|                 appsProvider: appsProvider); | ||||
|             if (widget.onSelected != null) { | ||||
|               widget.onSelected!(storedValues.keys | ||||
|                   .where((k) => storedValues[k]!.value) | ||||
|   | ||||
| @@ -757,6 +757,18 @@ class AppsProvider with ChangeNotifier { | ||||
|     await intent.launch(); | ||||
|   } | ||||
|  | ||||
|   addMissingCategories(SettingsProvider settingsProvider) { | ||||
|     var cats = settingsProvider.categories; | ||||
|     apps.forEach((key, value) { | ||||
|       for (var c in value.app.categories) { | ||||
|         if (!cats.containsKey(c)) { | ||||
|           cats[c] = generateRandomLightColor().value; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     settingsProvider.setCategories(cats, appsProvider: this); | ||||
|   } | ||||
|  | ||||
|   Future<App?> checkUpdate(String appId) async { | ||||
|     App? currentApp = apps[appId]!.app; | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
| @@ -836,12 +848,6 @@ class AppsProvider with ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   Future<String> exportApps() async { | ||||
|     Directory? exportDir = Directory('/storage/emulated/0/Download'); | ||||
|     String path = 'Downloads'; // TODO: See if hardcoding this can be avoided | ||||
|     if (!exportDir.existsSync()) { | ||||
|       exportDir = await getExternalStorageDirectory(); | ||||
|       path = exportDir!.path; | ||||
|     } | ||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         await Permission.storage.request(); | ||||
| @@ -850,6 +856,18 @@ class AppsProvider with ChangeNotifier { | ||||
|         throw ObtainiumError(tr('storagePermissionDenied')); | ||||
|       } | ||||
|     } | ||||
|     Directory? exportDir = Directory('/storage/emulated/0/Download'); | ||||
|     String path = 'Downloads'; // TODO: See if hardcoding this can be avoided | ||||
|     var downloadsAccessible = false; | ||||
|     try { | ||||
|       downloadsAccessible = exportDir.existsSync(); | ||||
|     } catch (e) { | ||||
|       logs.add('Error accessing Downloads (will use fallback): $e'); | ||||
|     } | ||||
|     if (!downloadsAccessible) { | ||||
|       exportDir = await getExternalStorageDirectory(); | ||||
|       path = exportDir!.path; | ||||
|     } | ||||
|     File export = File( | ||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); | ||||
|     export.writeAsStringSync( | ||||
|   | ||||
| @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| @@ -139,6 +141,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); | ||||
|   } | ||||
| @@ -151,7 +162,22 @@ class SettingsProvider with ChangeNotifier { | ||||
|   Map<String, int> get categories => | ||||
|       Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}')); | ||||
|  | ||||
|   set categories(Map<String, int> cats) { | ||||
|   void setCategories(Map<String, int> cats, {AppsProvider? appsProvider}) { | ||||
|     if (appsProvider != null) { | ||||
|       List<App> changedApps = appsProvider.apps.values | ||||
|           .map((a) { | ||||
|             var n1 = a.app.categories.length; | ||||
|             a.app.categories.removeWhere((c) => !cats.keys.contains(c)); | ||||
|             return n1 > a.app.categories.length ? a.app : null; | ||||
|           }) | ||||
|           .where((element) => element != null) | ||||
|           .map((e) => e as App) | ||||
|           .toList(); | ||||
|       if (changedApps.isNotEmpty) { | ||||
|         appsProvider.saveApps(changedApps, | ||||
|             attemptToCorrectInstallStatus: false); | ||||
|       } | ||||
|     } | ||||
|     prefs?.setString('categories', jsonEncode(cats)); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -337,10 +337,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: markdown | ||||
|       sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b | ||||
|       sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.1" | ||||
|     version: "7.0.2" | ||||
|   matcher: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -553,34 +553,34 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394" | ||||
|       sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.20" | ||||
|     version: "2.1.0" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 | ||||
|       sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.17" | ||||
|     version: "2.1.0" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_foundation | ||||
|       sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" | ||||
|       sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.5" | ||||
|     version: "2.2.0" | ||||
|   shared_preferences_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_linux | ||||
|       sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" | ||||
|       sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.5" | ||||
|     version: "2.2.0" | ||||
|   shared_preferences_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -593,18 +593,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_web | ||||
|       sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" | ||||
|       sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.6" | ||||
|     version: "2.1.0" | ||||
|   shared_preferences_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_windows | ||||
|       sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" | ||||
|       sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.5" | ||||
|     version: "2.2.0" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -790,10 +790,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       sha256: "34f83c2f0f64c75ad75c77a2ccfc8d2e531afbe8ad41af1fd787d6d33336aa90" | ||||
|       sha256: "9e223788e1954087dac30d813dc151f8e12f09f1139f116ce20b5658893f3627" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.3" | ||||
|     version: "3.4.4" | ||||
|   webview_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -835,5 +835,5 @@ packages: | ||||
|     source: hosted | ||||
|     version: "6.2.2" | ||||
| sdks: | ||||
|   dart: ">=2.18.2 <3.0.0" | ||||
|   dart: ">=2.19.0 <3.0.0" | ||||
|   flutter: ">=3.4.0-17.0.pre" | ||||
|   | ||||
| @@ -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.16+138 # When changing this, update the tag in main() accordingly | ||||
| version: 0.11.22+144 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user