mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
			v0.11.17-b
			...
			v0.11.24-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 361a3e1bc2 | ||
|  | f33a26d4f4 | ||
|  | 7aaf56ec8c | ||
|  | ed120016d9 | ||
|  | e8cbac8657 | ||
|  | b66c13d319 | ||
|  | 782d055bc3 | ||
|  | d557746965 | ||
|  | e6b05d50b9 | ||
|  | dea635fa6a | ||
|  | 682026ed0a | ||
|  | 9fe8a200ef | ||
|  | 210100da2b | ||
|  | d52660235b | ||
|  | e386b5ab8a | ||
|  | abf7be222d | ||
|  | 4c5b9304c0 | ||
|  | 4cfe6af044 | ||
|  | 3f0c4068dd | ||
|  | 7981ca29c5 | ||
|  | 187efa8fc5 | ||
|  | cd27ff7f2d | 
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "Kategorie hinzufügen", |     "addCategory": "Kategorie hinzufügen", | ||||||
|     "label": "Bezeichnung", |     "label": "Bezeichnung", | ||||||
|     "language": "Sprache", |     "language": "Sprache", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "Speicherberechtigung verweigert", |     "storagePermissionDenied": "Speicherberechtigung verweigert", | ||||||
|     "selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.", |     "selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.", | ||||||
|     "filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern", |     "filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern", | ||||||
|   | |||||||
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "Add Category", |     "addCategory": "Add Category", | ||||||
|     "label": "Label", |     "label": "Label", | ||||||
|     "language": "Language", |     "language": "Language", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "Storage permission denied", |     "storagePermissionDenied": "Storage permission denied", | ||||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", |     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||||
|   | |||||||
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "اضافه کردن دسته", |     "addCategory": "اضافه کردن دسته", | ||||||
|     "label": "برچسب", |     "label": "برچسب", | ||||||
|     "language": "زبان", |     "language": "زبان", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "مجوز ذخیره سازی رد شد", |     "storagePermissionDenied": "مجوز ذخیره سازی رد شد", | ||||||
|     "selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.", |     "selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.", | ||||||
|     "filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید", |     "filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید", | ||||||
|   | |||||||
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "Ajouter une catégorie", |     "addCategory": "Ajouter une catégorie", | ||||||
|     "label": "Étiquette", |     "label": "Étiquette", | ||||||
|     "language": "Langue", |     "language": "Langue", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "Autorisation de stockage refusée", |     "storagePermissionDenied": "Autorisation de stockage refusée", | ||||||
|     "selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.", |     "selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.", | ||||||
|     "filterAPKsByRegEx": "Filtrer les APK par expression régulière", |     "filterAPKsByRegEx": "Filtrer les APK par expression régulière", | ||||||
|   | |||||||
| @@ -206,6 +206,7 @@ | |||||||
|     "addCategory": "Új kategória", |     "addCategory": "Új kategória", | ||||||
|     "label": "Címke", |     "label": "Címke", | ||||||
|     "language": "Nyelv", |     "language": "Nyelv", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "Tárhely engedély megtagadva", |     "storagePermissionDenied": "Tárhely engedély megtagadva", | ||||||
|     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", |     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", | ||||||
|     "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel", |     "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel", | ||||||
| @@ -219,7 +220,7 @@ | |||||||
|     "importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)", |     "importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)", | ||||||
|     "versionDetection": "Verzió érzékelés", |     "versionDetection": "Verzió érzékelés", | ||||||
|     "standardVersionDetection": "Alapért. verzió érzékelés", |     "standardVersionDetection": "Alapért. verzió érzékelés", | ||||||
|     "groupByCategory": "Group by Category", |     "groupByCategory": "Csoportosítás Kategória alapján", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Eltávolítja az alkalmazást?", |         "one": "Eltávolítja az alkalmazást?", | ||||||
|         "other": "Eltávolítja az alkalmazást?" |         "other": "Eltávolítja az alkalmazást?" | ||||||
|   | |||||||
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "Aggiungi categoria", |     "addCategory": "Aggiungi categoria", | ||||||
|     "label": "Etichetta", |     "label": "Etichetta", | ||||||
|     "language": "Lingua", |     "language": "Lingua", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "Accesso ai file non autorizzato", |     "storagePermissionDenied": "Accesso ai file non autorizzato", | ||||||
|     "selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.", |     "selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.", | ||||||
|     "filterAPKsByRegEx": "Filtra file APK con espressioni regolari", |     "filterAPKsByRegEx": "Filtra file APK con espressioni regolari", | ||||||
|   | |||||||
| @@ -207,6 +207,7 @@ | |||||||
|     "addCategory": "カテゴリを追加", |     "addCategory": "カテゴリを追加", | ||||||
|     "label": "ラベル", |     "label": "ラベル", | ||||||
|     "language": "言語", |     "language": "言語", | ||||||
|  |     "copiedToClipboard": "クリップボードにコピーしました", | ||||||
|     "storagePermissionDenied": "ストレージ権限が拒否されました", |     "storagePermissionDenied": "ストレージ権限が拒否されました", | ||||||
|     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", |     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", | ||||||
|     "filterAPKsByRegEx": "正規表現でAPKを絞り込む", |     "filterAPKsByRegEx": "正規表現でAPKを絞り込む", | ||||||
|   | |||||||
| @@ -208,6 +208,7 @@ | |||||||
|     "addCategory": "添加类别", |     "addCategory": "添加类别", | ||||||
|     "label": "标签", |     "label": "标签", | ||||||
|     "language": "语言", |     "language": "语言", | ||||||
|  |     "copiedToClipboard": "Copied to Clipboard", | ||||||
|     "storagePermissionDenied": "存储权限已被拒绝", |     "storagePermissionDenied": "存储权限已被拒绝", | ||||||
|     "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", |     "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", | ||||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", |     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||||
|   | |||||||
| @@ -14,12 +14,14 @@ class FDroid extends AppSource { | |||||||
|   @override |   @override | ||||||
|   String standardizeURL(String url) { |   String standardizeURL(String url) { | ||||||
|     RegExp standardUrlRegExB = |     RegExp standardUrlRegExB = | ||||||
|         RegExp('^https?://$host/+[^/]+/+packages/+[^/]+'); |         RegExp('^https?://(cloudflare\\.)?$host/+[^/]+/+packages/+[^/]+'); | ||||||
|     RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); |     RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase()); | ||||||
|     if (match != null) { |     if (match != null) { | ||||||
|       url = 'https://$host/packages/${Uri.parse(url).pathSegments.last}'; |       url = | ||||||
|  |           'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}'; | ||||||
|     } |     } | ||||||
|     RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+'); |     RegExp standardUrlRegExA = | ||||||
|  |         RegExp('^https?://(cloudflare\\.)?$host/+packages/+[^/]+'); | ||||||
|     match = standardUrlRegExA.firstMatch(url.toLowerCase()); |     match = standardUrlRegExA.firstMatch(url.toLowerCase()); | ||||||
|     if (match == null) { |     if (match == null) { | ||||||
|       throw InvalidURLError(name); |       throw InvalidURLError(name); | ||||||
| @@ -61,9 +63,10 @@ class FDroid extends AppSource { | |||||||
|     Map<String, dynamic> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     String? appId = tryInferringAppId(standardUrl); |     String? appId = tryInferringAppId(standardUrl); | ||||||
|  |     String host = Uri.parse(standardUrl).host; | ||||||
|     return getAPKUrlsFromFDroidPackagesAPIResponse( |     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|         await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), |         await get(Uri.parse('https://$host/api/v1/packages/$appId')), | ||||||
|         'https://f-droid.org/repo/$appId', |         'https://$host/repo/$appId', | ||||||
|         standardUrl); |         standardUrl); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -185,9 +185,11 @@ class GitHub extends AppSource { | |||||||
|       Map<String, String> urlsWithDescriptions = {}; |       Map<String, String> urlsWithDescriptions = {}; | ||||||
|       for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) { |       for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) { | ||||||
|         urlsWithDescriptions.addAll({ |         urlsWithDescriptions.addAll({ | ||||||
|           e['html_url'] as String: e['description'] != null |           e['html_url'] as String: | ||||||
|               ? e['description'] as String |               ((e['archived'] == true ? '[ARCHIVED] ' : '') + | ||||||
|               : tr('noDescription') |                   (e['description'] != null | ||||||
|  |                       ? e['description'] as String | ||||||
|  |                       : tr('noDescription'))) | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|       return urlsWithDescriptions; |       return urlsWithDescriptions; | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import 'package:html/parser.dart'; | import 'package:html/parser.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/app_sources/html.dart'; |  | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| @@ -29,24 +28,41 @@ class Mullvad extends AppSource { | |||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, dynamic> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     var details = await HTML().getLatestAPKDetails( |     Response res = await get(Uri.parse('$standardUrl/en/download/android')); | ||||||
|         '$standardUrl/en/download/android', additionalSettings); |     if (res.statusCode == 200) { | ||||||
|     var fileName = details.apkUrls[0].split('/').last; |       var versions = parse(res.body) | ||||||
|     var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(fileName); |           .querySelectorAll('p') | ||||||
|     if (versionMatch == null) { |           .map((e) => e.innerHtml) | ||||||
|       throw NoVersionError(); |           .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 | // ignore: implementation_imports | ||||||
| import 'package:easy_localization/src/localization.dart'; | import 'package:easy_localization/src/localization.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.11.17'; | const String currentVersion = '0.11.24'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
|   | |||||||
| @@ -334,11 +334,10 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|           ], |           ], | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|     Widget getSourcesListWidget() => Expanded( |     Widget getSourcesListWidget() => Column( | ||||||
|             child: Column( |             crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                 mainAxisAlignment: MainAxisAlignment.center, |             children: [ | ||||||
|                 children: [ |  | ||||||
|               const SizedBox( |               const SizedBox( | ||||||
|                 height: 48, |                 height: 48, | ||||||
|               ), |               ), | ||||||
| @@ -365,16 +364,17 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                             fontStyle: FontStyle.italic), |                             fontStyle: FontStyle.italic), | ||||||
|                       ))) |                       ))) | ||||||
|                   .toList() |                   .toList() | ||||||
|             ])); |             ]); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|         backgroundColor: Theme.of(context).colorScheme.surface, |         backgroundColor: Theme.of(context).colorScheme.surface, | ||||||
|         body: CustomScrollView(slivers: <Widget>[ |         body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[ | ||||||
|           CustomAppBar(title: tr('addApp')), |           CustomAppBar(title: tr('addApp')), | ||||||
|           SliverFillRemaining( |           SliverToBoxAdapter( | ||||||
|             child: Padding( |             child: Padding( | ||||||
|                 padding: const EdgeInsets.all(16), |                 padding: const EdgeInsets.all(16), | ||||||
|                 child: Column( |                 child: Column( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, |                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       getUrlInputRow(), |                       getUrlInputRow(), | ||||||
|   | |||||||
| @@ -61,6 +61,12 @@ class _AppPageState extends State<AppPage> { | |||||||
|                         mode: LaunchMode.externalApplication); |                         mode: LaunchMode.externalApplication); | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
|  |                 onLongPress: () { | ||||||
|  |                   Clipboard.setData(ClipboardData(text: app?.app.url ?? '')); | ||||||
|  |                   ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||||||
|  |                     content: Text(tr('copiedToClipboard')), | ||||||
|  |                   )); | ||||||
|  |                 }, | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   app?.app.url ?? '', |                   app?.app.url ?? '', | ||||||
|                   textAlign: TextAlign.center, |                   textAlign: TextAlign.center, | ||||||
| @@ -147,7 +153,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|               height: 25, |               height: 25, | ||||||
|             ), |             ), | ||||||
|             Text( |             Text( | ||||||
|               app?.installedInfo?.name ?? app?.app.name ?? tr('app'), |               app?.app.name ?? tr('app'), | ||||||
|               textAlign: TextAlign.center, |               textAlign: TextAlign.center, | ||||||
|               style: Theme.of(context).textTheme.displayLarge, |               style: Theme.of(context).textTheme.displayLarge, | ||||||
|             ), |             ), | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     var appsProvider = context.watch<AppsProvider>(); |     var appsProvider = context.watch<AppsProvider>(); | ||||||
|     var settingsProvider = context.watch<SettingsProvider>(); |     var settingsProvider = context.watch<SettingsProvider>(); | ||||||
|  |     var sourceProvider = SourceProvider(); | ||||||
|     var listedApps = appsProvider.apps.values.toList(); |     var listedApps = appsProvider.apps.values.toList(); | ||||||
|     var currentFilterIsUpdatesOnly = |     var currentFilterIsUpdatesOnly = | ||||||
|         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); |         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); | ||||||
| @@ -110,6 +111,11 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               .isEmpty) { |               .isEmpty) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|  |       if (filter.sourceFilter.isNotEmpty && | ||||||
|  |           sourceProvider.getSource(app.app.url).runtimeType.toString() != | ||||||
|  |               filter.sourceFilter) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|       return true; |       return true; | ||||||
|     }).toList(); |     }).toList(); | ||||||
|  |  | ||||||
| @@ -448,7 +454,8 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     .app |                     .app | ||||||
|                     .categories |                     .categories | ||||||
|                     .map((e) => |                     .map((e) => | ||||||
|                         Color(settingsProvider.categories[e]!).withAlpha(255)) |                         Color(settingsProvider.categories[e] ?? transparent) | ||||||
|  |                             .withAlpha(255)) | ||||||
|                     .toList(), |                     .toList(), | ||||||
|                 Color(transparent) |                 Color(transparent) | ||||||
|               ])), |               ])), | ||||||
| @@ -734,14 +741,12 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pinSelectedApps() { |     pinSelectedApps() { | ||||||
|       () { |       var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; | ||||||
|         var pinStatus = selectedApps.where((element) => element.pinned).isEmpty; |       appsProvider.saveApps(selectedApps.map((e) { | ||||||
|         appsProvider.saveApps(selectedApps.map((e) { |         e.pinned = pinStatus; | ||||||
|           e.pinned = pinStatus; |         return e; | ||||||
|           return e; |       }).toList()); | ||||||
|         }).toList()); |       Navigator.of(context).pop(); | ||||||
|         Navigator.of(context).pop(); |  | ||||||
|       }; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     resetSelectedAppsInstallStatuses() { |     resetSelectedAppsInstallStatuses() { | ||||||
| @@ -893,6 +898,19 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                   GeneratedFormSwitch('nonInstalledApps', |                   GeneratedFormSwitch('nonInstalledApps', | ||||||
|                       label: tr('nonInstalledApps'), |                       label: tr('nonInstalledApps'), | ||||||
|                       defaultValue: vals['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: [ |               additionalWidgets: [ | ||||||
| @@ -1016,20 +1034,23 @@ class AppsFilter { | |||||||
|   late bool includeUptodate; |   late bool includeUptodate; | ||||||
|   late bool includeNonInstalled; |   late bool includeNonInstalled; | ||||||
|   late Set<String> categoryFilter; |   late Set<String> categoryFilter; | ||||||
|  |   late String sourceFilter; | ||||||
|  |  | ||||||
|   AppsFilter( |   AppsFilter( | ||||||
|       {this.nameFilter = '', |       {this.nameFilter = '', | ||||||
|       this.authorFilter = '', |       this.authorFilter = '', | ||||||
|       this.includeUptodate = true, |       this.includeUptodate = true, | ||||||
|       this.includeNonInstalled = true, |       this.includeNonInstalled = true, | ||||||
|       this.categoryFilter = const {}}); |       this.categoryFilter = const {}, | ||||||
|  |       this.sourceFilter = ''}); | ||||||
|  |  | ||||||
|   Map<String, dynamic> toFormValuesMap() { |   Map<String, dynamic> toFormValuesMap() { | ||||||
|     return { |     return { | ||||||
|       'appName': nameFilter, |       'appName': nameFilter, | ||||||
|       'author': authorFilter, |       'author': authorFilter, | ||||||
|       'upToDateApps': includeUptodate, |       'upToDateApps': includeUptodate, | ||||||
|       'nonInstalledApps': includeNonInstalled |       'nonInstalledApps': includeNonInstalled, | ||||||
|  |       'sourceFilter': sourceFilter | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -1038,6 +1059,7 @@ class AppsFilter { | |||||||
|     authorFilter = values['author']!; |     authorFilter = values['author']!; | ||||||
|     includeUptodate = values['upToDateApps']; |     includeUptodate = values['upToDateApps']; | ||||||
|     includeNonInstalled = values['nonInstalledApps']; |     includeNonInstalled = values['nonInstalledApps']; | ||||||
|  |     sourceFilter = values['sourceFilter']; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) => |   bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) => | ||||||
| @@ -1045,5 +1067,6 @@ class AppsFilter { | |||||||
|       nameFilter.trim() == other.nameFilter.trim() && |       nameFilter.trim() == other.nameFilter.trim() && | ||||||
|       includeUptodate == other.includeUptodate && |       includeUptodate == other.includeUptodate && | ||||||
|       includeNonInstalled == other.includeNonInstalled && |       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); |             showError(tr('importedX', args: [plural('apps', value)]), context); | ||||||
|           }); |           }); | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart'; | |||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/main.dart'; | import 'package:obtainium/main.dart'; | ||||||
|  | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/logs_provider.dart'; | import 'package:obtainium/providers/logs_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -444,6 +445,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     var settingsProvider = context.watch<SettingsProvider>(); |     var settingsProvider = context.watch<SettingsProvider>(); | ||||||
|  |     var appsProvider = context.watch<AppsProvider>(); | ||||||
|     storedValues = settingsProvider.categories.map((key, value) => MapEntry( |     storedValues = settingsProvider.categories.map((key, value) => MapEntry( | ||||||
|         key, |         key, | ||||||
|         MapEntry(value, |         MapEntry(value, | ||||||
| @@ -467,8 +469,9 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | |||||||
|           if (!isBuilding) { |           if (!isBuilding) { | ||||||
|             storedValues = |             storedValues = | ||||||
|                 values['categories'] as Map<String, MapEntry<int, bool>>; |                 values['categories'] as Map<String, MapEntry<int, bool>>; | ||||||
|             settingsProvider.categories = |             settingsProvider.setCategories( | ||||||
|                 storedValues.map((key, value) => MapEntry(key, value.key)); |                 storedValues.map((key, value) => MapEntry(key, value.key)), | ||||||
|  |                 appsProvider: appsProvider); | ||||||
|             if (widget.onSelected != null) { |             if (widget.onSelected != null) { | ||||||
|               widget.onSelected!(storedValues.keys |               widget.onSelected!(storedValues.keys | ||||||
|                   .where((k) => storedValues[k]!.value) |                   .where((k) => storedValues[k]!.value) | ||||||
|   | |||||||
| @@ -670,6 +670,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|     for (var app in apps) { |     for (var app in apps) { | ||||||
|       AppInfo? info = await getInstalledInfo(app.id); |       AppInfo? info = await getInstalledInfo(app.id); | ||||||
|       app.name = info?.name ?? app.name; |       app.name = info?.name ?? app.name; | ||||||
|  |       if (app.additionalSettings['appName']?.toString().isNotEmpty == true) { | ||||||
|  |         app.name = app.additionalSettings['appName'].toString().trim(); | ||||||
|  |       } | ||||||
|       if (attemptToCorrectInstallStatus) { |       if (attemptToCorrectInstallStatus) { | ||||||
|         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; |         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; | ||||||
|       } |       } | ||||||
| @@ -757,6 +760,18 @@ class AppsProvider with ChangeNotifier { | |||||||
|     await intent.launch(); |     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 { |   Future<App?> checkUpdate(String appId) async { | ||||||
|     App? currentApp = apps[appId]!.app; |     App? currentApp = apps[appId]!.app; | ||||||
|     SourceProvider sourceProvider = SourceProvider(); |     SourceProvider sourceProvider = SourceProvider(); | ||||||
| @@ -836,12 +851,6 @@ class AppsProvider with ChangeNotifier { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> exportApps() async { |   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 DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { | ||||||
|       if (await Permission.storage.isDenied) { |       if (await Permission.storage.isDenied) { | ||||||
|         await Permission.storage.request(); |         await Permission.storage.request(); | ||||||
| @@ -850,6 +859,18 @@ class AppsProvider with ChangeNotifier { | |||||||
|         throw ObtainiumError(tr('storagePermissionDenied')); |         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( |     File export = File( | ||||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); |         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); | ||||||
|     export.writeAsStringSync( |     export.writeAsStringSync( | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:fluttertoast/fluttertoast.dart'; | import 'package:fluttertoast/fluttertoast.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/main.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:permission_handler/permission_handler.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| @@ -160,7 +162,22 @@ class SettingsProvider with ChangeNotifier { | |||||||
|   Map<String, int> get categories => |   Map<String, int> get categories => | ||||||
|       Map<String, int>.from(jsonDecode(prefs?.getString('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)); |     prefs?.setString('categories', jsonEncode(cats)); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -278,7 +278,8 @@ class AppSource { | |||||||
|               return regExValidator(value); |               return regExValidator(value); | ||||||
|             } |             } | ||||||
|           ]) |           ]) | ||||||
|     ] |     ], | ||||||
|  |     [GeneratedFormTextField('appName', label: tr('appName'), required: false)] | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   // Previous 2 variables combined into one at runtime for convenient usage |   // Previous 2 variables combined into one at runtime for convenient usage | ||||||
| @@ -362,7 +363,7 @@ class SourceProvider { | |||||||
|     url = preStandardizeUrl(url); |     url = preStandardizeUrl(url); | ||||||
|     AppSource? source; |     AppSource? source; | ||||||
|     for (var s in sources.where((element) => element.host != null)) { |     for (var s in sources.where((element) => element.host != null)) { | ||||||
|       if (url.contains('://${s.host}')) { |       if (RegExp('://(.+\\.)?${s.host}').hasMatch(url)) { | ||||||
|         source = s; |         source = s; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| @@ -427,8 +428,10 @@ class SourceProvider { | |||||||
|       throw NoAPKError(); |       throw NoAPKError(); | ||||||
|     } |     } | ||||||
|     String apkVersion = apk.version.replaceAll('/', '-'); |     String apkVersion = apk.version.replaceAll('/', '-'); | ||||||
|     var name = currentApp?.name.trim() ?? |     var name = currentApp != null ? currentApp.name.trim() : ''; | ||||||
|         apk.names.name[0].toUpperCase() + apk.names.name.substring(1); |     name = name.isNotEmpty | ||||||
|  |         ? name | ||||||
|  |         : apk.names.name[0].toUpperCase() + apk.names.name.substring(1); | ||||||
|     return App( |     return App( | ||||||
|         currentApp?.id ?? |         currentApp?.id ?? | ||||||
|             source.tryInferringAppId(standardUrl, |             source.tryInferringAppId(standardUrl, | ||||||
| @@ -436,9 +439,7 @@ class SourceProvider { | |||||||
|             generateTempID(standardUrl, additionalSettings), |             generateTempID(standardUrl, additionalSettings), | ||||||
|         standardUrl, |         standardUrl, | ||||||
|         apk.names.author[0].toUpperCase() + apk.names.author.substring(1), |         apk.names.author[0].toUpperCase() + apk.names.author.substring(1), | ||||||
|         name.trim().isNotEmpty |         name, | ||||||
|             ? name |  | ||||||
|             : apk.names.name[0].toUpperCase() + apk.names.name.substring(1), |  | ||||||
|         currentApp?.installedVersion, |         currentApp?.installedVersion, | ||||||
|         apkVersion, |         apkVersion, | ||||||
|         apk.apkUrls, |         apk.apkUrls, | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -337,10 +337,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: markdown |       name: markdown | ||||||
|       sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b |       sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.0.1" |     version: "7.0.2" | ||||||
|   matcher: |   matcher: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -553,34 +553,34 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences |       name: shared_preferences | ||||||
|       sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394" |       sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.20" |     version: "2.1.0" | ||||||
|   shared_preferences_android: |   shared_preferences_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_android |       name: shared_preferences_android | ||||||
|       sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 |       sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.17" |     version: "2.1.0" | ||||||
|   shared_preferences_foundation: |   shared_preferences_foundation: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_foundation |       name: shared_preferences_foundation | ||||||
|       sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" |       sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.5" |     version: "2.2.0" | ||||||
|   shared_preferences_linux: |   shared_preferences_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_linux |       name: shared_preferences_linux | ||||||
|       sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" |       sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.5" |     version: "2.2.0" | ||||||
|   shared_preferences_platform_interface: |   shared_preferences_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -593,18 +593,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_web |       name: shared_preferences_web | ||||||
|       sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" |       sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.6" |     version: "2.1.0" | ||||||
|   shared_preferences_windows: |   shared_preferences_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_windows |       name: shared_preferences_windows | ||||||
|       sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" |       sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.5" |     version: "2.2.0" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -790,10 +790,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_android |       name: webview_flutter_android | ||||||
|       sha256: "34f83c2f0f64c75ad75c77a2ccfc8d2e531afbe8ad41af1fd787d6d33336aa90" |       sha256: "9e223788e1954087dac30d813dc151f8e12f09f1139f116ce20b5658893f3627" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.4.3" |     version: "3.4.4" | ||||||
|   webview_flutter_platform_interface: |   webview_flutter_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -835,5 +835,5 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.2.2" |     version: "6.2.2" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=2.18.2 <3.0.0" |   dart: ">=2.19.0 <3.0.0" | ||||||
|   flutter: ">=3.4.0-17.0.pre" |   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 | # 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 | # 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. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 0.11.17+139 # When changing this, update the tag in main() accordingly | version: 0.11.24+146 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user