mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 21:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			44 Commits
		
	
	
		
			v0.9.10-be
			...
			v0.10.4-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f43e5a2ff1 | ||
|  | b72aa8273e | ||
|  | 520f186e4a | ||
|  | e1e97672cf | ||
|  | 1494bcd013 | ||
|  | 3457a0a12f | ||
|  | b165400a6e | ||
|  | c47bf937f1 | ||
|  | 2e19a8c04c | ||
|  | 05d4da86ec | ||
|  | e9d1b04d54 | ||
|  | cff5334c25 | ||
|  | a55346fc22 | ||
|  | 885df678e5 | ||
|  | bf7b0c5702 | ||
|  | 2972da4609 | ||
|  | b8567af98e | ||
|  | ea62c68b40 | ||
|  | 08a5af0449 | ||
|  | 36f327c16e | ||
|  | 768213cb34 | ||
|  | e888fb7120 | ||
|  | 1fb68dd674 | ||
|  | 5c4bb8f84c | ||
|  | 1c8e759494 | ||
|  | 081c2a07d2 | ||
|  | 02751fe8fa | ||
|  | 95f3362a84 | ||
|  | b68cf5a1be | ||
|  | 4eb7499591 | ||
|  | 98fafe2aa4 | ||
|  | 9bac74aadd | ||
|  | 0a93117bf0 | ||
|  | 451cc41c45 | ||
|  | 3b449d0982 | ||
|  | 1863f55372 | ||
|  | 0c4b8ac79d | ||
|  | e287087753 | ||
|  | 82bcc46d42 | ||
|  | 1f26188ec6 | ||
|  | 794c3e1a81 | ||
|  | 16369b4adf | ||
|  | 8f16f745be | ||
|  | 8ddeb3d776 | 
| @@ -1,4 +1,4 @@ | ||||
| #  Obtainium | ||||
| #  Obtainium | ||||
|  | ||||
| Get Android App Updates Directly From the Source. | ||||
|  | ||||
| @@ -9,6 +9,7 @@ Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to | ||||
| Currently supported App sources: | ||||
| - [GitHub](https://github.com/) | ||||
| - [GitLab](https://gitlab.com/) | ||||
| - [Codeberg](https://codeberg.org/) | ||||
| - [F-Droid](https://f-droid.org/) | ||||
| - [IzzyOnDroid](https://android.izzysoft.de/) | ||||
| - [Mullvad](https://mullvad.net/en/) | ||||
| @@ -18,6 +19,8 @@ Currently supported App sources: | ||||
| - Third Party F-Droid Repos | ||||
|   - Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo` | ||||
| - [Steam](https://store.steampowered.com/mobile) | ||||
| - "HTML" (Fallback) | ||||
|   - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) | ||||
|  | ||||
| ## Limitations | ||||
| - App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected. | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.1 KiB | 
| @@ -2,4 +2,5 @@ | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|   <background android:drawable="@color/ic_launcher_background"/> | ||||
|   <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
|   <monochrome android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/graphics/icon_small.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/graphics/icon_small.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7.6 KiB | 
| @@ -210,6 +210,7 @@ | ||||
|     "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" | ||||
|   | ||||
| @@ -210,6 +210,7 @@ | ||||
|     "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,8 +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", | ||||
|     "language": "Nyelv", | ||||
|     "storagePermissionDenied": "Tárhely engedély megtagadva", | ||||
|     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", | ||||
|     "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" | ||||
|   | ||||
| @@ -210,6 +210,7 @@ | ||||
|     "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" | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|     "ok": "OK", | ||||
|     "and": "と", | ||||
|     "startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始", | ||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is  {}", | ||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}", | ||||
|     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", | ||||
|     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", | ||||
|     "firstRun": "これがObtainiumの最初の実行です", | ||||
| @@ -65,13 +65,13 @@ | ||||
|     "estimateInBrackets": "(推定)", | ||||
|     "selectAll": "すべて選択", | ||||
|     "deselectN": "{}件の選択を解除", | ||||
|     "xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。", | ||||
|     "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", | ||||
|     "removeSelectedApps": "選択したアプリを削除する", | ||||
|     "updateX": "{}をアップデートする", | ||||
|     "installX": "{}をインストールする", | ||||
|     "updateX": "{} をアップデートする", | ||||
|     "installX": "{} をインストールする", | ||||
|     "markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする", | ||||
|     "changeX": "{}を変更する", | ||||
|     "changeX": "{} を変更する", | ||||
|     "installUpdateApps": "アプリのインストール/アップデート", | ||||
|     "installUpdateSelectedApps": "選択したアプリのインストール/アップデート", | ||||
|     "onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。", | ||||
| @@ -145,23 +145,23 @@ | ||||
|     "appNotFound": "アプリが見つかりません", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-エクスポート", | ||||
|     "pickAnAPK": "APKを選択", | ||||
|     "appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ", | ||||
|     "deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。", | ||||
|     "appHasMoreThanOnePackage": "{} は複数のパッケージが存在します: ", | ||||
|     "deviceSupportsXArch": "お使いのデバイスは {} CPUアーキテクチャに対応しています。", | ||||
|     "deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:", | ||||
|     "warning": "警告", | ||||
|     "sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?", | ||||
|     "updatesAvailable": "アップデートが利用可能", | ||||
|     "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", | ||||
|     "noNewUpdates": "新しいアップデートはありません", | ||||
|     "xHasAnUpdate": "{}のアップデートが利用可能です", | ||||
|     "xHasAnUpdate": "{} のアップデートが利用可能です", | ||||
|     "appsUpdated": "アプリをアップデートしました", | ||||
|     "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", | ||||
|     "xWasUpdatedToY": "{}が{}にアップデートされました", | ||||
|     "xWasUpdatedToY": "{} が {} にアップデートされました", | ||||
|     "errorCheckingUpdates": "アップデート確認中のエラー", | ||||
|     "errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知", | ||||
|     "appsRemoved": "削除されたアプリ", | ||||
|     "appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する", | ||||
|     "xWasRemovedDueToErrorY": "このエラーのため、{}は削除されました: {}", | ||||
|     "xWasRemovedDueToErrorY": "このエラーのため、{} は削除されました: {}", | ||||
|     "completeAppInstallation": "アプリのインストールを完了する", | ||||
|     "obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。", | ||||
|     "completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。", | ||||
| @@ -209,7 +209,8 @@ | ||||
|     "addCategory": "カテゴリを追加", | ||||
|     "label": "ラベル", | ||||
|     "language": "言語", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "storagePermissionDenied": "ストレージ権限が拒否されました", | ||||
|     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||
| @@ -247,11 +248,11 @@ | ||||
|         "other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "{}とさらに{}個のアプリのアップデートが利用可能です", | ||||
|         "other": "{}とさらに{}個のアプリのアップデートが利用可能です" | ||||
|         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です", | ||||
|         "other": "{} とさらに {} 個のアプリのアップデートが利用可能です" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{}とさらに{}個のアプリがアップデートされました", | ||||
|         "other": "{}とさらに{}個のアプリがアップデートされました" | ||||
|         "one": "{} とさらに {} 個のアプリがアップデートされました", | ||||
|         "other": "{} とさらに {} 個のアプリがアップデートされました" | ||||
|     } | ||||
| } | ||||
| @@ -12,7 +12,7 @@ | ||||
|     "ok": "好的", | ||||
|     "and": "和", | ||||
|     "startedBgUpdateTask": "开始后台检查更新任务", | ||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is  {}", | ||||
|     "bgUpdateIgnoreAfterIs": "下次后台更新检查  {}", | ||||
|     "startedActualBGUpdateCheck": "后台检查更新已开始", | ||||
|     "bgUpdateTaskFinished": "后台检查更新已完成", | ||||
|     "firstRun": "这是你第一次运行 Obtainium", | ||||
| @@ -199,17 +199,18 @@ | ||||
|     "downloadNotifDescription": "通知用户下载进度", | ||||
|     "noAPKFound": "未找到安装包", | ||||
|     "noVersionDetection": "无版本检测", | ||||
|     "categorize": "Categorize", | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "noCategories": "No Categories", | ||||
|     "deleteCategoriesQuestion": "Delete Categories?", | ||||
|     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "categorize": "归档", | ||||
|     "categories": "归档", | ||||
|     "category": "类别", | ||||
|     "noCategory": "无类别", | ||||
|     "noCategories": "无类别", | ||||
|     "deleteCategoriesQuestion": "删除所有类别?", | ||||
|     "categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别", | ||||
|     "addCategory": "添加类别", | ||||
|     "label": "标签", | ||||
|     "language": "语言", | ||||
|     "storagePermissionDenied": "存储权限已被拒绝", | ||||
|     "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", | ||||
|         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" | ||||
|   | ||||
							
								
								
									
										157
									
								
								lib/app_sources/codeberg.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								lib/app_sources/codeberg.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| import 'dart:convert'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class Codeberg extends AppSource { | ||||
|   Codeberg() { | ||||
|     host = 'codeberg.org'; | ||||
|  | ||||
|     additionalSourceSpecificSettingFormItems = []; | ||||
|  | ||||
|     additionalSourceAppSpecificSettingFormItems = [ | ||||
|       [ | ||||
|         GeneratedFormSwitch('includePrereleases', | ||||
|             label: tr('includePrereleases'), defaultValue: false) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormSwitch('fallbackToOlderReleases', | ||||
|             label: tr('fallbackToOlderReleases'), defaultValue: true) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormTextField('filterReleaseTitlesByRegEx', | ||||
|             label: tr('filterReleaseTitlesByRegEx'), | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 if (value == null || value.isEmpty) { | ||||
|                   return null; | ||||
|                 } | ||||
|                 try { | ||||
|                   RegExp(value); | ||||
|                 } catch (e) { | ||||
|                   return tr('invalidRegEx'); | ||||
|                 } | ||||
|                 return null; | ||||
|               } | ||||
|             ]) | ||||
|       ] | ||||
|     ]; | ||||
|  | ||||
|     canSearch = true; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|     RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
|     } | ||||
|     return url.substring(0, match.end); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => | ||||
|       '$standardUrl/releases'; | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     bool includePrereleases = additionalSettings['includePrereleases']; | ||||
|     bool fallbackToOlderReleases = | ||||
|         additionalSettings['fallbackToOlderReleases']; | ||||
|     String? regexFilter = | ||||
|         (additionalSettings['filterReleaseTitlesByRegEx'] as String?) | ||||
|                     ?.isNotEmpty == | ||||
|                 true | ||||
|             ? additionalSettings['filterReleaseTitlesByRegEx'] | ||||
|             : null; | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var releases = jsonDecode(res.body) as List<dynamic>; | ||||
|  | ||||
|       List<String> getReleaseAPKUrls(dynamic release) => | ||||
|           (release['assets'] as List<dynamic>?) | ||||
|               ?.map((e) { | ||||
|                 return e['name'] != null && e['browser_download_url'] != null | ||||
|                     ? MapEntry(e['name'] as String, | ||||
|                         e['browser_download_url'] as String) | ||||
|                     : const MapEntry('', ''); | ||||
|               }) | ||||
|               .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||
|               .map((e) => e.value) | ||||
|               .toList() ?? | ||||
|           []; | ||||
|  | ||||
|       dynamic targetRelease; | ||||
|  | ||||
|       for (int i = 0; i < releases.length; i++) { | ||||
|         if (!fallbackToOlderReleases && i > 0) break; | ||||
|         if (!includePrereleases && releases[i]['prerelease'] == true) { | ||||
|           continue; | ||||
|         } | ||||
|         if (releases[i]['draft'] == true) { | ||||
|           // Draft releases not supported | ||||
|         } | ||||
|         var nameToFilter = releases[i]['name'] as String?; | ||||
|         if (nameToFilter == null || nameToFilter.trim().isEmpty) { | ||||
|           // Some leave titles empty so tag is used | ||||
|           nameToFilter = releases[i]['tag_name'] as String; | ||||
|         } | ||||
|         if (regexFilter != null && | ||||
|             !RegExp(regexFilter).hasMatch(nameToFilter.trim())) { | ||||
|           continue; | ||||
|         } | ||||
|         var apkUrls = getReleaseAPKUrls(releases[i]); | ||||
|         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { | ||||
|           continue; | ||||
|         } | ||||
|         targetRelease = releases[i]; | ||||
|         targetRelease['apkUrls'] = apkUrls; | ||||
|         break; | ||||
|       } | ||||
|       if (targetRelease == null) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       String? version = targetRelease['tag_name']; | ||||
|       if (version == null) { | ||||
|         throw NoVersionError(); | ||||
|       } | ||||
|       return APKDetails(version, targetRelease['apkUrls'] as List<String>, | ||||
|           getAppNames(standardUrl)); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   AppNames getAppNames(String standardUrl) { | ||||
|     String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); | ||||
|     List<String> names = temp.substring(temp.indexOf('/') + 1).split('/'); | ||||
|     return AppNames(names[0], names[1]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<Map<String, String>> search(String query) async { | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100')); | ||||
|     if (res.statusCode == 200) { | ||||
|       Map<String, String> urlsWithDescriptions = {}; | ||||
|       for (var e in (jsonDecode(res.body)['data'] as List<dynamic>)) { | ||||
|         urlsWithDescriptions.addAll({ | ||||
|           e['html_url'] as String: e['description'] != null | ||||
|               ? e['description'] as String | ||||
|               : tr('noDescription') | ||||
|         }); | ||||
|       } | ||||
|       return urlsWithDescriptions; | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -15,6 +15,7 @@ class GitHub extends AppSource { | ||||
|     additionalSourceSpecificSettingFormItems = [ | ||||
|       GeneratedFormTextField('github-creds', | ||||
|           label: tr('githubPATLabel'), | ||||
|           password: true, | ||||
|           required: false, | ||||
|           additionalValidators: [ | ||||
|             (value) { | ||||
| @@ -140,10 +141,13 @@ class GitHub extends AppSource { | ||||
|         if (!includePrereleases && releases[i]['prerelease'] == true) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         var nameToFilter = releases[i]['name'] as String?; | ||||
|         if (nameToFilter == null || nameToFilter.trim().isEmpty) { | ||||
|           // Some leave titles empty so tag is used | ||||
|           nameToFilter = releases[i]['tag_name'] as String; | ||||
|         } | ||||
|         if (regexFilter != null && | ||||
|             !RegExp(regexFilter) | ||||
|                 .hasMatch((releases[i]['name'] as String).trim())) { | ||||
|             !RegExp(regexFilter).hasMatch(nameToFilter.trim())) { | ||||
|           continue; | ||||
|         } | ||||
|         var apkUrls = getReleaseAPKUrls(releases[i]); | ||||
|   | ||||
							
								
								
									
										47
									
								
								lib/app_sources/html.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/app_sources/html.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class HTML extends AppSource { | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|     return url; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String? changeLogPageFromStandardUrl(String standardUrl) => null; | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var uri = Uri.parse(standardUrl); | ||||
|     Response res = await get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       List<String> links = parse(res.body) | ||||
|           .querySelectorAll('a') | ||||
|           .map((element) => element.attributes['href'] ?? '') | ||||
|           .where((element) => element.toLowerCase().endsWith('.apk')) | ||||
|           .toList(); | ||||
|       links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last)); | ||||
|       if (links.isEmpty) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       var rel = links.last; | ||||
|       var apkName = rel.split('/').last; | ||||
|       var version = apkName.substring(0, apkName.length - 4); | ||||
|       List<String> apkUrls = [rel] | ||||
|           .map((e) => e.toLowerCase().startsWith('http://') || | ||||
|                   e.toLowerCase().startsWith('https://') | ||||
|               ? e | ||||
|               : '${uri.origin}/$e') | ||||
|           .toList(); | ||||
|       return APKDetails(version, apkUrls, AppNames(uri.host, tr('app'))); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -3,7 +3,6 @@ import 'dart:math'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
|  | ||||
| abstract class GeneratedFormItem { | ||||
|   late String key; | ||||
| @@ -24,6 +23,7 @@ class GeneratedFormTextField extends GeneratedFormItem { | ||||
|   late bool required; | ||||
|   late int max; | ||||
|   late String? hint; | ||||
|   late bool password; | ||||
|  | ||||
|   GeneratedFormTextField(String key, | ||||
|       {String label = 'Input', | ||||
| @@ -32,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem { | ||||
|       List<String? Function(String? value)> additionalValidators = const [], | ||||
|       this.required = true, | ||||
|       this.max = 1, | ||||
|       this.hint}) | ||||
|       this.hint, | ||||
|       this.password = false}) | ||||
|       : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
| @@ -129,6 +130,21 @@ class GeneratedForm extends StatefulWidget { | ||||
|   State<GeneratedForm> createState() => _GeneratedFormState(); | ||||
| } | ||||
|  | ||||
| // Generates a random light color | ||||
| // Courtesy of ChatGPT 😭 (with a bugfix 🥳) | ||||
| Color generateRandomLightColor() { | ||||
|   // Create a random number generator | ||||
|   final Random random = Random(); | ||||
|  | ||||
|   // Generate random hue, saturation, and value values | ||||
|   final double hue = random.nextDouble() * 360; | ||||
|   final double saturation = 0.5 + random.nextDouble() * 0.5; | ||||
|   final double value = 0.9 + random.nextDouble() * 0.1; | ||||
|  | ||||
|   // Create a HSV color with the random values | ||||
|   return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor(); | ||||
| } | ||||
|  | ||||
| class _GeneratedFormState extends State<GeneratedForm> { | ||||
|   final _formKey = GlobalKey<FormState>(); | ||||
|   Map<String, dynamic> values = {}; | ||||
| @@ -153,21 +169,6 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|     widget.onValueChanges(returnValues, valid, isBuilding); | ||||
|   } | ||||
|  | ||||
|   // Generates a random light color | ||||
|   // Courtesy of ChatGPT 😭 (with a bugfix 🥳) | ||||
|   Color generateRandomLightColor() { | ||||
|     // Create a random number generator | ||||
|     final Random random = Random(); | ||||
|  | ||||
|     // Generate random hue, saturation, and value values | ||||
|     final double hue = random.nextDouble() * 360; | ||||
|     final double saturation = 0.5 + random.nextDouble() * 0.5; | ||||
|     final double value = 0.9 + random.nextDouble() * 0.1; | ||||
|  | ||||
|     // Create a HSV color with the random values | ||||
|     return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
| @@ -188,6 +189,9 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|         if (formItem is GeneratedFormTextField) { | ||||
|           final formFieldKey = GlobalKey<FormFieldState>(); | ||||
|           return TextFormField( | ||||
|             obscureText: formItem.password, | ||||
|             autocorrect: !formItem.password, | ||||
|             enableSuggestions: !formItem.password, | ||||
|             key: formFieldKey, | ||||
|             initialValue: values[formItem.key], | ||||
|             autovalidateMode: AutovalidateMode.onUserInteraction, | ||||
|   | ||||
| @@ -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.10'; | ||||
| const String currentVersion = '0.10.4'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
|   | ||||
| @@ -24,19 +24,22 @@ class AddAppPage extends StatefulWidget { | ||||
|  | ||||
| class _AddAppPageState extends State<AddAppPage> { | ||||
|   bool gettingAppInfo = false; | ||||
|   bool searching = false; | ||||
|  | ||||
|   String userInput = ''; | ||||
|   String searchQuery = ''; | ||||
|   AppSource? pickedSource; | ||||
|   Map<String, dynamic> additionalSettings = {}; | ||||
|   bool additionalSettingsValid = true; | ||||
|   String? category; | ||||
|   List<String> pickedCategories = []; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||
|  | ||||
|     bool doingSomething = gettingAppInfo || searching; | ||||
|  | ||||
|     changeUserInput(String input, bool valid, bool isBuilding) { | ||||
|       userInput = input; | ||||
|       if (!isBuilding) { | ||||
| @@ -127,9 +130,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; | ||||
| @@ -200,7 +201,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                           gettingAppInfo | ||||
|                               ? const CircularProgressIndicator() | ||||
|                               : ElevatedButton( | ||||
|                                   onPressed: gettingAppInfo || | ||||
|                                   onPressed: doingSomething || | ||||
|                                           pickedSource == null || | ||||
|                                           (pickedSource! | ||||
|                                                   .combinedAppSpecificSettingFormItems | ||||
| @@ -251,9 +252,12 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                               width: 16, | ||||
|                             ), | ||||
|                             ElevatedButton( | ||||
|                                 onPressed: searchQuery.isEmpty || gettingAppInfo | ||||
|                                 onPressed: searchQuery.isEmpty || doingSomething | ||||
|                                     ? null | ||||
|                                     : () { | ||||
|                                         setState(() { | ||||
|                                           searching = true; | ||||
|                                         }); | ||||
|                                         Future.wait(sourceProvider.sources | ||||
|                                                 .where((e) => e.canSearch) | ||||
|                                                 .map((e) => | ||||
| @@ -290,11 +294,15 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                           if (selectedUrls != null && | ||||
|                                               selectedUrls.isNotEmpty) { | ||||
|                                             changeUserInput( | ||||
|                                                 selectedUrls[0], true, true); | ||||
|                                                 selectedUrls[0], true, false); | ||||
|                                             addApp(resetUserInputAfter: true); | ||||
|                                           } | ||||
|                                         }).catchError((e) { | ||||
|                                           showError(e, context); | ||||
|                                         }).whenComplete(() { | ||||
|                                           setState(() { | ||||
|                                             searching = false; | ||||
|                                           }); | ||||
|                                         }); | ||||
|                                       }, | ||||
|                                 child: Text(tr('search'))) | ||||
| @@ -334,11 +342,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) | ||||
| @@ -338,7 +348,9 @@ class AppsPageState extends State<AppsPage> { | ||||
|         Row( | ||||
|           children: [ | ||||
|             selectedApps.isEmpty | ||||
|                 ? IconButton( | ||||
|                 ? TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                     onPressed: () { | ||||
|                       selectThese(sortedApps.map((e) => e.app).toList()); | ||||
|                     }, | ||||
| @@ -346,8 +358,10 @@ class AppsPageState extends State<AppsPage> { | ||||
|                       Icons.select_all_outlined, | ||||
|                       color: Theme.of(context).colorScheme.primary, | ||||
|                     ), | ||||
|                     tooltip: tr('selectAll')) | ||||
|                     label: Text(sortedApps.length.toString())) | ||||
|                 : TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                     onPressed: () { | ||||
|                       selectedApps.isEmpty | ||||
|                           ? selectThese(sortedApps.map((e) => e.app).toList()) | ||||
| @@ -362,307 +376,415 @@ class AppsPageState extends State<AppsPage> { | ||||
|                     label: Text(selectedApps.length.toString())), | ||||
|             const VerticalDivider(), | ||||
|             Expanded( | ||||
|                 child: Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|               children: [ | ||||
|                 selectedApps.isEmpty | ||||
|                     ? const SizedBox() | ||||
|                     : IconButton( | ||||
|                         visualDensity: VisualDensity.compact, | ||||
|                         onPressed: () { | ||||
|                           showDialog<Map<String, dynamic>?>( | ||||
|                               context: context, | ||||
|                               builder: (BuildContext ctx) { | ||||
|                                 return GeneratedFormModal( | ||||
|                                   title: tr('removeSelectedAppsQuestion'), | ||||
|                                   items: const [], | ||||
|                                   initValid: true, | ||||
|                                   message: tr( | ||||
|                                       'xWillBeRemovedButRemainInstalled', | ||||
|                                       args: [ | ||||
|                                         plural('apps', selectedApps.length) | ||||
|                                       ]), | ||||
|                                 ); | ||||
|                               }).then((values) { | ||||
|                             if (values != null) { | ||||
|                               appsProvider.removeApps( | ||||
|                                   selectedApps.map((e) => e.id).toList()); | ||||
|                             } | ||||
|                           }); | ||||
|                         }, | ||||
|                         tooltip: tr('removeSelectedApps'), | ||||
|                         icon: const Icon(Icons.delete_outline_outlined), | ||||
|                       ), | ||||
|                 IconButton( | ||||
|                     visualDensity: VisualDensity.compact, | ||||
|                     onPressed: appsProvider.areDownloadsRunning() || | ||||
|                             (existingUpdateIdsAllOrSelected.isEmpty && | ||||
|                                 newInstallIdsAllOrSelected.isEmpty && | ||||
|                                 trackOnlyUpdateIdsAllOrSelected.isEmpty) | ||||
|                         ? null | ||||
|                         : () { | ||||
|                             HapticFeedback.heavyImpact(); | ||||
|                             List<GeneratedFormItem> formItems = []; | ||||
|                             if (existingUpdateIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormSwitch('updates', | ||||
|                                   label: tr('updateX', args: [ | ||||
|                                     plural('apps', | ||||
|                                         existingUpdateIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   defaultValue: true)); | ||||
|                             } | ||||
|                             if (newInstallIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormSwitch('installs', | ||||
|                                   label: tr('installX', args: [ | ||||
|                                     plural('apps', | ||||
|                                         newInstallIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   defaultValue: existingUpdateIdsAllOrSelected | ||||
|                                       .isNotEmpty)); | ||||
|                             } | ||||
|                             if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormSwitch('trackonlies', | ||||
|                                   label: tr('markXTrackOnlyAsUpdated', args: [ | ||||
|                                     plural('apps', | ||||
|                                         trackOnlyUpdateIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   defaultValue: existingUpdateIdsAllOrSelected | ||||
|                                           .isNotEmpty || | ||||
|                                       newInstallIdsAllOrSelected.isNotEmpty)); | ||||
|                             } | ||||
|                             showDialog<Map<String, dynamic>?>( | ||||
|                                 context: context, | ||||
|                                 builder: (BuildContext ctx) { | ||||
|                                   var totalApps = existingUpdateIdsAllOrSelected | ||||
|                                           .length + | ||||
|                                       newInstallIdsAllOrSelected.length + | ||||
|                                       trackOnlyUpdateIdsAllOrSelected.length; | ||||
|                                   return GeneratedFormModal( | ||||
|                                     title: tr('changeX', | ||||
|                                         args: [plural('apps', totalApps)]), | ||||
|                                     items: formItems.map((e) => [e]).toList(), | ||||
|                                     initValid: true, | ||||
|                                   ); | ||||
|                                 }).then((values) { | ||||
|                               if (values != null) { | ||||
|                                 if (values.isEmpty) { | ||||
|                                   values = getDefaultValuesFromFormItems( | ||||
|                                       [formItems]); | ||||
|                                 } | ||||
|                                 bool shouldInstallUpdates = | ||||
|                                     values['updates'] == true; | ||||
|                                 bool shouldInstallNew = | ||||
|                                     values['installs'] == true; | ||||
|                                 bool shouldMarkTrackOnlies = | ||||
|                                     values['trackonlies'] == true; | ||||
|                                 (() async { | ||||
|                                   if (shouldInstallNew || | ||||
|                                       shouldInstallUpdates) { | ||||
|                                     await settingsProvider | ||||
|                                         .getInstallPermission(); | ||||
|                                   } | ||||
|                                 })() | ||||
|                                     .then((_) { | ||||
|                                   List<String> toInstall = []; | ||||
|                                   if (shouldInstallUpdates) { | ||||
|                                     toInstall | ||||
|                                         .addAll(existingUpdateIdsAllOrSelected); | ||||
|                                   } | ||||
|                                   if (shouldInstallNew) { | ||||
|                                     toInstall | ||||
|                                         .addAll(newInstallIdsAllOrSelected); | ||||
|                                   } | ||||
|                                   if (shouldMarkTrackOnlies) { | ||||
|                                     toInstall.addAll( | ||||
|                                         trackOnlyUpdateIdsAllOrSelected); | ||||
|                                   } | ||||
|                                   appsProvider | ||||
|                                       .downloadAndInstallLatestApps(toInstall, | ||||
|                                           globalNavigatorKey.currentContext) | ||||
|                                       .catchError((e) { | ||||
|                                     showError(e, context); | ||||
|                 child: SingleChildScrollView( | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     child: Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                       children: [ | ||||
|                         IconButton( | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                           onPressed: selectedApps.isEmpty | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   showDialog<Map<String, dynamic>?>( | ||||
|                                       context: context, | ||||
|                                       builder: (BuildContext ctx) { | ||||
|                                         return GeneratedFormModal( | ||||
|                                           title: | ||||
|                                               tr('removeSelectedAppsQuestion'), | ||||
|                                           items: const [], | ||||
|                                           initValid: true, | ||||
|                                           message: tr( | ||||
|                                               'xWillBeRemovedButRemainInstalled', | ||||
|                                               args: [ | ||||
|                                                 plural( | ||||
|                                                     'apps', selectedApps.length) | ||||
|                                               ]), | ||||
|                                         ); | ||||
|                                       }).then((values) { | ||||
|                                     if (values != null) { | ||||
|                                       appsProvider.removeApps(selectedApps | ||||
|                                           .map((e) => e.id) | ||||
|                                           .toList()); | ||||
|                                     } | ||||
|                                   }); | ||||
|                                 }); | ||||
|                               } | ||||
|                             }); | ||||
|                           }, | ||||
|                     tooltip: selectedApps.isEmpty | ||||
|                         ? tr('installUpdateApps') | ||||
|                         : tr('installUpdateSelectedApps'), | ||||
|                     icon: const Icon( | ||||
|                       Icons.file_download_outlined, | ||||
|                     )), | ||||
|                 selectedApps.isEmpty | ||||
|                     ? const SizedBox() | ||||
|                     : IconButton( | ||||
|                         visualDensity: VisualDensity.compact, | ||||
|                         onPressed: () { | ||||
|                           showDialog( | ||||
|                               context: context, | ||||
|                               builder: (BuildContext ctx) { | ||||
|                                 return AlertDialog( | ||||
|                                   scrollable: true, | ||||
|                                   content: Padding( | ||||
|                                     padding: const EdgeInsets.only(top: 6), | ||||
|                                     child: Row( | ||||
|                                         mainAxisAlignment: | ||||
|                                             MainAxisAlignment.spaceAround, | ||||
|                                         children: [ | ||||
|                                           IconButton( | ||||
|                                               onPressed: | ||||
|                                                   appsProvider | ||||
|                                                           .areDownloadsRunning() | ||||
|                                                       ? null | ||||
|                                                       : () { | ||||
|                                                           showDialog( | ||||
|                                                               context: context, | ||||
|                                                               builder: | ||||
|                                                                   (BuildContext | ||||
|                                                                       ctx) { | ||||
|                                                                 return AlertDialog( | ||||
|                                                                   title: Text(tr( | ||||
|                                                                       'markXSelectedAppsAsUpdated', | ||||
|                                                                       args: [ | ||||
|                                                                         selectedApps | ||||
|                                                                             .length | ||||
|                                                                             .toString() | ||||
|                                                                       ])), | ||||
|                                                                   content: Text( | ||||
|                                                                     tr('onlyWorksWithNonEVDApps'), | ||||
|                                                                     style: const TextStyle( | ||||
|                                                                         fontWeight: | ||||
|                                                                             FontWeight | ||||
|                                                                                 .bold, | ||||
|                                                                         fontStyle: | ||||
|                                                                             FontStyle.italic), | ||||
|                                                                   ), | ||||
|                                                                   actions: [ | ||||
|                                                                     TextButton( | ||||
|                                                                         onPressed: | ||||
|                                                                             () { | ||||
|                                                                           Navigator.of(context) | ||||
|                                                                               .pop(); | ||||
|                                                                         }, | ||||
|                                                                         child: Text( | ||||
|                                                                             tr('no'))), | ||||
|                                                                     TextButton( | ||||
|                                                                         onPressed: | ||||
|                                                                             () { | ||||
|                                                                           HapticFeedback | ||||
|                                                                               .selectionClick(); | ||||
|                                                                           appsProvider | ||||
|                                                                               .saveApps(selectedApps.map((a) { | ||||
|                                                                             if (a.installedVersion != | ||||
|                                                                                 null) { | ||||
|                                                                               a.installedVersion = a.latestVersion; | ||||
|                                                                             } | ||||
|                                                                             return a; | ||||
|                                                                           }).toList()); | ||||
|                                 }, | ||||
|                           tooltip: tr('removeSelectedApps'), | ||||
|                           icon: const Icon(Icons.delete_outline_outlined), | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                             visualDensity: VisualDensity.compact, | ||||
|                             onPressed: appsProvider.areDownloadsRunning() || | ||||
|                                     (existingUpdateIdsAllOrSelected.isEmpty && | ||||
|                                         newInstallIdsAllOrSelected.isEmpty && | ||||
|                                         trackOnlyUpdateIdsAllOrSelected.isEmpty) | ||||
|                                 ? null | ||||
|                                 : () { | ||||
|                                     HapticFeedback.heavyImpact(); | ||||
|                                     List<GeneratedFormItem> formItems = []; | ||||
|                                     if (existingUpdateIdsAllOrSelected | ||||
|                                         .isNotEmpty) { | ||||
|                                       formItems.add(GeneratedFormSwitch( | ||||
|                                           'updates', | ||||
|                                           label: tr('updateX', args: [ | ||||
|                                             plural( | ||||
|                                                 'apps', | ||||
|                                                 existingUpdateIdsAllOrSelected | ||||
|                                                     .length) | ||||
|                                           ]), | ||||
|                                           defaultValue: true)); | ||||
|                                     } | ||||
|                                     if (newInstallIdsAllOrSelected.isNotEmpty) { | ||||
|                                       formItems.add(GeneratedFormSwitch( | ||||
|                                           'installs', | ||||
|                                           label: tr('installX', args: [ | ||||
|                                             plural( | ||||
|                                                 'apps', | ||||
|                                                 newInstallIdsAllOrSelected | ||||
|                                                     .length) | ||||
|                                           ]), | ||||
|                                           defaultValue: | ||||
|                                               existingUpdateIdsAllOrSelected | ||||
|                                                   .isNotEmpty)); | ||||
|                                     } | ||||
|                                     if (trackOnlyUpdateIdsAllOrSelected | ||||
|                                         .isNotEmpty) { | ||||
|                                       formItems.add(GeneratedFormSwitch( | ||||
|                                           'trackonlies', | ||||
|                                           label: tr('markXTrackOnlyAsUpdated', | ||||
|                                               args: [ | ||||
|                                                 plural( | ||||
|                                                     'apps', | ||||
|                                                     trackOnlyUpdateIdsAllOrSelected | ||||
|                                                         .length) | ||||
|                                               ]), | ||||
|                                           defaultValue: | ||||
|                                               existingUpdateIdsAllOrSelected | ||||
|                                                       .isNotEmpty || | ||||
|                                                   newInstallIdsAllOrSelected | ||||
|                                                       .isNotEmpty)); | ||||
|                                     } | ||||
|                                     showDialog<Map<String, dynamic>?>( | ||||
|                                         context: context, | ||||
|                                         builder: (BuildContext ctx) { | ||||
|                                           var totalApps = | ||||
|                                               existingUpdateIdsAllOrSelected.length + | ||||
|                                                   newInstallIdsAllOrSelected | ||||
|                                                       .length + | ||||
|                                                   trackOnlyUpdateIdsAllOrSelected | ||||
|                                                       .length; | ||||
|                                           return GeneratedFormModal( | ||||
|                                             title: tr('changeX', args: [ | ||||
|                                               plural('apps', totalApps) | ||||
|                                             ]), | ||||
|                                             items: formItems | ||||
|                                                 .map((e) => [e]) | ||||
|                                                 .toList(), | ||||
|                                             initValid: true, | ||||
|                                           ); | ||||
|                                         }).then((values) { | ||||
|                                       if (values != null) { | ||||
|                                         if (values.isEmpty) { | ||||
|                                           values = | ||||
|                                               getDefaultValuesFromFormItems( | ||||
|                                                   [formItems]); | ||||
|                                         } | ||||
|                                         bool shouldInstallUpdates = | ||||
|                                             values['updates'] == true; | ||||
|                                         bool shouldInstallNew = | ||||
|                                             values['installs'] == true; | ||||
|                                         bool shouldMarkTrackOnlies = | ||||
|                                             values['trackonlies'] == true; | ||||
|                                         (() async { | ||||
|                                           if (shouldInstallNew || | ||||
|                                               shouldInstallUpdates) { | ||||
|                                             await settingsProvider | ||||
|                                                 .getInstallPermission(); | ||||
|                                           } | ||||
|                                         })() | ||||
|                                             .then((_) { | ||||
|                                           List<String> toInstall = []; | ||||
|                                           if (shouldInstallUpdates) { | ||||
|                                             toInstall.addAll( | ||||
|                                                 existingUpdateIdsAllOrSelected); | ||||
|                                           } | ||||
|                                           if (shouldInstallNew) { | ||||
|                                             toInstall.addAll( | ||||
|                                                 newInstallIdsAllOrSelected); | ||||
|                                           } | ||||
|                                           if (shouldMarkTrackOnlies) { | ||||
|                                             toInstall.addAll( | ||||
|                                                 trackOnlyUpdateIdsAllOrSelected); | ||||
|                                           } | ||||
|                                           appsProvider | ||||
|                                               .downloadAndInstallLatestApps( | ||||
|                                                   toInstall, | ||||
|                                                   globalNavigatorKey | ||||
|                                                       .currentContext) | ||||
|                                               .catchError((e) { | ||||
|                                             showError(e, context); | ||||
|                                           }); | ||||
|                                         }); | ||||
|                                       } | ||||
|                                     }); | ||||
|                                   }, | ||||
|                             tooltip: selectedApps.isEmpty | ||||
|                                 ? tr('installUpdateApps') | ||||
|                                 : tr('installUpdateSelectedApps'), | ||||
|                             icon: const Icon( | ||||
|                               Icons.file_download_outlined, | ||||
|                             )), | ||||
|                         IconButton( | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                           onPressed: selectedApps.isEmpty | ||||
|                               ? null | ||||
|                               : () 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: !showPrompt | ||||
|                                                       ? 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), | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                           onPressed: selectedApps.isEmpty | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   showDialog( | ||||
|                                       context: context, | ||||
|                                       builder: (BuildContext ctx) { | ||||
|                                         return AlertDialog( | ||||
|                                           scrollable: true, | ||||
|                                           content: Padding( | ||||
|                                             padding: | ||||
|                                                 const EdgeInsets.only(top: 6), | ||||
|                                             child: Row( | ||||
|                                                 mainAxisAlignment: | ||||
|                                                     MainAxisAlignment | ||||
|                                                         .spaceAround, | ||||
|                                                 children: [ | ||||
|                                                   IconButton( | ||||
|                                                       onPressed: appsProvider | ||||
|                                                               .areDownloadsRunning() | ||||
|                                                           ? null | ||||
|                                                           : () { | ||||
|                                                               showDialog( | ||||
|                                                                   context: | ||||
|                                                                       context, | ||||
|                                                                   builder: | ||||
|                                                                       (BuildContext | ||||
|                                                                           ctx) { | ||||
|                                                                     return AlertDialog( | ||||
|                                                                       title: Text(tr( | ||||
|                                                                           'markXSelectedAppsAsUpdated', | ||||
|                                                                           args: [ | ||||
|                                                                             selectedApps.length.toString() | ||||
|                                                                           ])), | ||||
|                                                                       content: | ||||
|                                                                           Text( | ||||
|                                                                         tr('onlyWorksWithNonEVDApps'), | ||||
|                                                                         style: const TextStyle( | ||||
|                                                                             fontWeight: | ||||
|                                                                                 FontWeight.bold, | ||||
|                                                                             fontStyle: FontStyle.italic), | ||||
|                                                                       ), | ||||
|                                                                       actions: [ | ||||
|                                                                         TextButton( | ||||
|                                                                             onPressed: | ||||
|                                                                                 () { | ||||
|                                                                               Navigator.of(context).pop(); | ||||
|                                                                             }, | ||||
|                                                                             child: | ||||
|                                                                                 Text(tr('no'))), | ||||
|                                                                         TextButton( | ||||
|                                                                             onPressed: | ||||
|                                                                                 () { | ||||
|                                                                               HapticFeedback.selectionClick(); | ||||
|                                                                               appsProvider.saveApps(selectedApps.map((a) { | ||||
|                                                                                 if (a.installedVersion != null) { | ||||
|                                                                                   a.installedVersion = a.latestVersion; | ||||
|                                                                                 } | ||||
|                                                                                 return a; | ||||
|                                                                               }).toList()); | ||||
|  | ||||
|                                                                           Navigator.of(context) | ||||
|                                                                               .pop(); | ||||
|                                                                         }, | ||||
|                                                                         child: Text( | ||||
|                                                                             tr('yes'))) | ||||
|                                                                   ], | ||||
|                                                                 ); | ||||
|                                                               }).whenComplete(() { | ||||
|                                                             Navigator.of( | ||||
|                                                                     context) | ||||
|                                                                 .pop(); | ||||
|                                                           }); | ||||
|                                                         }, | ||||
|                                               tooltip: | ||||
|                                                   tr('markSelectedAppsUpdated'), | ||||
|                                               icon: const Icon(Icons.done)), | ||||
|                                           IconButton( | ||||
|                                             onPressed: () { | ||||
|                                               var pinStatus = selectedApps | ||||
|                                                   .where((element) => | ||||
|                                                       element.pinned) | ||||
|                                                   .isEmpty; | ||||
|                                               appsProvider.saveApps( | ||||
|                                                   selectedApps.map((e) { | ||||
|                                                 e.pinned = pinStatus; | ||||
|                                                 return e; | ||||
|                                               }).toList()); | ||||
|                                               Navigator.of(context).pop(); | ||||
|                                             }, | ||||
|                                             tooltip: selectedApps | ||||
|                                                     .where((element) => | ||||
|                                                         element.pinned) | ||||
|                                                     .isEmpty | ||||
|                                                 ? tr('pinToTop') | ||||
|                                                 : tr('unpinFromTop'), | ||||
|                                             icon: Icon(selectedApps | ||||
|                                                     .where((element) => | ||||
|                                                         element.pinned) | ||||
|                                                     .isEmpty | ||||
|                                                 ? Icons.bookmark_outline_rounded | ||||
|                                                 : Icons | ||||
|                                                     .bookmark_remove_outlined), | ||||
|                                                                               Navigator.of(context).pop(); | ||||
|                                                                             }, | ||||
|                                                                             child: | ||||
|                                                                                 Text(tr('yes'))) | ||||
|                                                                       ], | ||||
|                                                                     ); | ||||
|                                                                   }).whenComplete(() { | ||||
|                                                                 Navigator.of( | ||||
|                                                                         context) | ||||
|                                                                     .pop(); | ||||
|                                                               }); | ||||
|                                                             }, | ||||
|                                                       tooltip: tr( | ||||
|                                                           'markSelectedAppsUpdated'), | ||||
|                                                       icon: const Icon( | ||||
|                                                           Icons.done)), | ||||
|                                                   IconButton( | ||||
|                                                     onPressed: () { | ||||
|                                                       var pinStatus = | ||||
|                                                           selectedApps | ||||
|                                                               .where((element) => | ||||
|                                                                   element | ||||
|                                                                       .pinned) | ||||
|                                                               .isEmpty; | ||||
|                                                       appsProvider.saveApps( | ||||
|                                                           selectedApps.map((e) { | ||||
|                                                         e.pinned = pinStatus; | ||||
|                                                         return e; | ||||
|                                                       }).toList()); | ||||
|                                                       Navigator.of(context) | ||||
|                                                           .pop(); | ||||
|                                                     }, | ||||
|                                                     tooltip: selectedApps | ||||
|                                                             .where((element) => | ||||
|                                                                 element.pinned) | ||||
|                                                             .isEmpty | ||||
|                                                         ? tr('pinToTop') | ||||
|                                                         : tr('unpinFromTop'), | ||||
|                                                     icon: Icon(selectedApps | ||||
|                                                             .where((element) => | ||||
|                                                                 element.pinned) | ||||
|                                                             .isEmpty | ||||
|                                                         ? Icons | ||||
|                                                             .bookmark_outline_rounded | ||||
|                                                         : Icons | ||||
|                                                             .bookmark_remove_outlined), | ||||
|                                                   ), | ||||
|                                                   IconButton( | ||||
|                                                     onPressed: () { | ||||
|                                                       String urls = ''; | ||||
|                                                       for (var a | ||||
|                                                           in selectedApps) { | ||||
|                                                         urls += '${a.url}\n'; | ||||
|                                                       } | ||||
|                                                       urls = urls.substring( | ||||
|                                                           0, urls.length - 1); | ||||
|                                                       Share.share(urls, | ||||
|                                                           subject: tr( | ||||
|                                                               'selectedAppURLsFromObtainium')); | ||||
|                                                       Navigator.of(context) | ||||
|                                                           .pop(); | ||||
|                                                     }, | ||||
|                                                     tooltip: tr( | ||||
|                                                         'shareSelectedAppURLs'), | ||||
|                                                     icon: | ||||
|                                                         const Icon(Icons.share), | ||||
|                                                   ), | ||||
|                                                   IconButton( | ||||
|                                                     onPressed: () { | ||||
|                                                       showDialog( | ||||
|                                                           context: context, | ||||
|                                                           builder: (BuildContext | ||||
|                                                               ctx) { | ||||
|                                                             return GeneratedFormModal( | ||||
|                                                               title: tr( | ||||
|                                                                   'resetInstallStatusForSelectedAppsQuestion'), | ||||
|                                                               items: const [], | ||||
|                                                               initValid: true, | ||||
|                                                               message: tr( | ||||
|                                                                   'installStatusOfXWillBeResetExplanation', | ||||
|                                                                   args: [ | ||||
|                                                                     plural( | ||||
|                                                                         'app', | ||||
|                                                                         selectedApps | ||||
|                                                                             .length) | ||||
|                                                                   ]), | ||||
|                                                             ); | ||||
|                                                           }).then((values) { | ||||
|                                                         if (values != null) { | ||||
|                                                           appsProvider.saveApps( | ||||
|                                                               selectedApps | ||||
|                                                                   .map((e) { | ||||
|                                                             e.installedVersion = | ||||
|                                                                 null; | ||||
|                                                             return e; | ||||
|                                                           }).toList()); | ||||
|                                                         } | ||||
|                                                       }).whenComplete(() { | ||||
|                                                         Navigator.of(context) | ||||
|                                                             .pop(); | ||||
|                                                       }); | ||||
|                                                     }, | ||||
|                                                     tooltip: tr( | ||||
|                                                         'resetInstallStatus'), | ||||
|                                                     icon: const Icon(Icons | ||||
|                                                         .restore_page_outlined), | ||||
|                                                   ), | ||||
|                                                 ]), | ||||
|                                           ), | ||||
|                                           IconButton( | ||||
|                                             onPressed: () { | ||||
|                                               String urls = ''; | ||||
|                                               for (var a in selectedApps) { | ||||
|                                                 urls += '${a.url}\n'; | ||||
|                                               } | ||||
|                                               urls = urls.substring( | ||||
|                                                   0, urls.length - 1); | ||||
|                                               Share.share(urls, | ||||
|                                                   subject: tr( | ||||
|                                                       'selectedAppURLsFromObtainium')); | ||||
|                                               Navigator.of(context).pop(); | ||||
|                                             }, | ||||
|                                             tooltip: tr('shareSelectedAppURLs'), | ||||
|                                             icon: const Icon(Icons.share), | ||||
|                                           ), | ||||
|                                           IconButton( | ||||
|                                             onPressed: () { | ||||
|                                               showDialog( | ||||
|                                                   context: context, | ||||
|                                                   builder: (BuildContext ctx) { | ||||
|                                                     return GeneratedFormModal( | ||||
|                                                       title: tr( | ||||
|                                                           'resetInstallStatusForSelectedAppsQuestion'), | ||||
|                                                       items: const [], | ||||
|                                                       initValid: true, | ||||
|                                                       message: tr( | ||||
|                                                           'installStatusOfXWillBeResetExplanation', | ||||
|                                                           args: [ | ||||
|                                                             plural( | ||||
|                                                                 'app', | ||||
|                                                                 selectedApps | ||||
|                                                                     .length) | ||||
|                                                           ]), | ||||
|                                                     ); | ||||
|                                                   }).then((values) { | ||||
|                                                 if (values != null) { | ||||
|                                                   appsProvider.saveApps( | ||||
|                                                       selectedApps.map((e) { | ||||
|                                                     e.installedVersion = null; | ||||
|                                                     return e; | ||||
|                                                   }).toList()); | ||||
|                                                 } | ||||
|                                               }).whenComplete(() { | ||||
|                                                 Navigator.of(context).pop(); | ||||
|                                               }); | ||||
|                                             }, | ||||
|                                             tooltip: tr('resetInstallStatus'), | ||||
|                                             icon: const Icon( | ||||
|                                                 Icons.restore_page_outlined), | ||||
|                                           ), | ||||
|                                         ]), | ||||
|                                   ), | ||||
|                                 ); | ||||
|                               }); | ||||
|                         }, | ||||
|                         tooltip: tr('more'), | ||||
|                         icon: const Icon(Icons.more_horiz), | ||||
|                       ), | ||||
|               ], | ||||
|             )), | ||||
|                                         ); | ||||
|                                       }); | ||||
|                                 }, | ||||
|                           tooltip: tr('more'), | ||||
|                           icon: const Icon(Icons.more_horiz), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ))), | ||||
|             const VerticalDivider(), | ||||
|             IconButton( | ||||
|               visualDensity: VisualDensity.compact, | ||||
| @@ -688,12 +810,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 +910,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); | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:file_picker/file_picker.dart'; | ||||
| @@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|   Widget build(BuildContext context) { | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     var appsProvider = context.read<AppsProvider>(); | ||||
|     var settingsProvider = context.read<SettingsProvider>(); | ||||
|     var outlineButtonStyle = ButtonStyle( | ||||
|       shape: MaterialStateProperty.all( | ||||
|         StadiumBorder( | ||||
| @@ -100,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                               appsProvider | ||||
|                                                   .importApps(data) | ||||
|                                                   .then((value) { | ||||
|                                                 var cats = | ||||
|                                                     settingsProvider.categories; | ||||
|                                                 appsProvider.apps | ||||
|                                                     .forEach((key, value) { | ||||
|                                                   for (var c | ||||
|                                                       in value.app.categories) { | ||||
|                                                     if (!cats.containsKey(c)) { | ||||
|                                                       cats[c] = | ||||
|                                                           generateRandomLightColor() | ||||
|                                                               .value; | ||||
|                                                     } | ||||
|                                                   } | ||||
|                                                 }); | ||||
|                                                 settingsProvider.categories = | ||||
|                                                     cats; | ||||
|                                                 showError( | ||||
|                                                     tr('importedX', args: [ | ||||
|                                                       plural('apps', value) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -247,10 +247,7 @@ class AppsProvider with ChangeNotifier { | ||||
|         !(await canDowngradeApps())) { | ||||
|       throw DowngradeError(); | ||||
|     } | ||||
|     if (appInfo == null || | ||||
|         int.parse(newInfo.buildNumber) > appInfo.versionCode!) { | ||||
|       await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); | ||||
|     } | ||||
|     await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); | ||||
|     apps[file.appId]!.app.installedVersion = | ||||
|         apps[file.appId]!.app.latestVersion; | ||||
|     // Don't correct install status as installation may not be done yet | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -7,11 +7,13 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/dom.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/apkmirror.dart'; | ||||
| import 'package:obtainium/app_sources/codeberg.dart'; | ||||
| import 'package:obtainium/app_sources/fdroid.dart'; | ||||
| import 'package:obtainium/app_sources/fdroidrepo.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/app_sources/gitlab.dart'; | ||||
| import 'package:obtainium/app_sources/izzyondroid.dart'; | ||||
| import 'package:obtainium/app_sources/html.dart'; | ||||
| import 'package:obtainium/app_sources/mullvad.dart'; | ||||
| import 'package:obtainium/app_sources/signal.dart'; | ||||
| import 'package:obtainium/app_sources/sourceforge.dart'; | ||||
| @@ -19,7 +21,6 @@ import 'package:obtainium/app_sources/steammobile.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/mass_app_sources/githubstars.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
|  | ||||
| class AppNames { | ||||
|   late String author; | ||||
| @@ -48,7 +49,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 +62,7 @@ class App { | ||||
|       this.additionalSettings, | ||||
|       this.lastUpdateCheck, | ||||
|       this.pinned, | ||||
|       {this.category}); | ||||
|       {this.categories = const []}); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
| @@ -103,6 +104,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 +122,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,12 +149,16 @@ class App { | ||||
|         'additionalSettings': jsonEncode(additionalSettings), | ||||
|         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, | ||||
|         'pinned': pinned, | ||||
|         'category': category | ||||
|         'categories': categories | ||||
|       }; | ||||
| } | ||||
|  | ||||
| // Ensure the input is starts with HTTPS and has no WWW | ||||
| preStandardizeUrl(String url) { | ||||
|   var firstDotIndex = url.indexOf('.'); | ||||
|   if (!(firstDotIndex >= 0 && firstDotIndex != url.length - 1)) { | ||||
|     throw UnsupportedURLError(); | ||||
|   } | ||||
|   if (url.toLowerCase().indexOf('http://') != 0 && | ||||
|       url.toLowerCase().indexOf('https://') != 0) { | ||||
|     url = 'https://$url'; | ||||
| @@ -259,6 +274,7 @@ class SourceProvider { | ||||
|   List<AppSource> sources = [ | ||||
|     GitHub(), | ||||
|     GitLab(), | ||||
|     Codeberg(), | ||||
|     FDroid(), | ||||
|     IzzyOnDroid(), | ||||
|     Mullvad(), | ||||
| @@ -266,7 +282,8 @@ class SourceProvider { | ||||
|     SourceForge(), | ||||
|     APKMirror(), | ||||
|     FDroidRepo(), | ||||
|     SteamMobile() | ||||
|     SteamMobile(), | ||||
|     HTML() // This should ALWAYS be the last option as they are tried in order | ||||
|   ]; | ||||
|  | ||||
|   // Add more mass url source classes here so they are available via the service | ||||
| @@ -360,11 +377,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 | ||||
|   | ||||
							
								
								
									
										67
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -28,7 +28,7 @@ packages: | ||||
|       name: args | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.3.1" | ||||
|     version: "2.3.2" | ||||
|   async: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -56,7 +56,7 @@ packages: | ||||
|       name: checked_yaml | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.1" | ||||
|     version: "2.0.2" | ||||
|   cli_util: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -182,7 +182,7 @@ packages: | ||||
|       name: file_picker | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "5.2.4" | ||||
|     version: "5.2.5" | ||||
|   flutter: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -286,7 +286,7 @@ packages: | ||||
|       name: image | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.2.2" | ||||
|     version: "3.3.0" | ||||
|   install_plugin_v2: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -321,7 +321,7 @@ packages: | ||||
|       name: json_annotation | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "4.7.0" | ||||
|     version: "4.8.0" | ||||
|   lints: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -356,7 +356,7 @@ packages: | ||||
|       name: mime | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.0.3" | ||||
|     version: "1.0.4" | ||||
|   nested: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -419,7 +419,7 @@ packages: | ||||
|       name: path_provider_macos | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.6" | ||||
|     version: "2.0.7" | ||||
|   path_provider_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -531,35 +531,28 @@ packages: | ||||
|       name: shared_preferences | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.15" | ||||
|     version: "2.0.17" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.14" | ||||
|   shared_preferences_ios: | ||||
|     version: "2.0.15" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_ios | ||||
|       name: shared_preferences_foundation | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.1" | ||||
|     version: "2.1.2" | ||||
|   shared_preferences_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_linux | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.2" | ||||
|   shared_preferences_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_macos | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.4" | ||||
|     version: "2.1.3" | ||||
|   shared_preferences_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -580,7 +573,7 @@ packages: | ||||
|       name: shared_preferences_windows | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.2" | ||||
|     version: "2.1.3" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -599,14 +592,14 @@ packages: | ||||
|       name: sqflite | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.2.2" | ||||
|     version: "2.2.3" | ||||
|   sqflite_common: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_common | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.4.0+2" | ||||
|     version: "2.4.1" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -634,7 +627,7 @@ packages: | ||||
|       name: synchronized | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.0+3" | ||||
|     version: "3.0.1" | ||||
|   term_glyph: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -655,7 +648,7 @@ packages: | ||||
|       name: timezone | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.9.0" | ||||
|     version: "0.9.1" | ||||
|   typed_data: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -669,35 +662,35 @@ packages: | ||||
|       name: url_launcher | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.1.7" | ||||
|     version: "6.1.8" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.0.22" | ||||
|     version: "6.0.23" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_ios | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "6.0.17" | ||||
|     version: "6.0.18" | ||||
|   url_launcher_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_linux | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "3.0.2" | ||||
|   url_launcher_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_macos | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "3.0.2" | ||||
|   url_launcher_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -711,14 +704,14 @@ packages: | ||||
|       name: url_launcher_web | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.13" | ||||
|     version: "2.0.14" | ||||
|   url_launcher_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_windows | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "3.0.2" | ||||
|   uuid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -739,28 +732,28 @@ packages: | ||||
|       name: webview_flutter | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "4.0.1" | ||||
|     version: "4.0.2" | ||||
|   webview_flutter_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.1.1" | ||||
|     version: "3.2.1" | ||||
|   webview_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_platform_interface | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|     version: "2.0.1" | ||||
|   webview_flutter_wkwebview: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_wkwebview | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "3.0.2" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -774,7 +767,7 @@ packages: | ||||
|       name: xdg_directories | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.2.0+2" | ||||
|     version: "0.2.0+3" | ||||
|   xml: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 0.9.10+98 # When changing this, update the tag in main() accordingly | ||||
| version: 0.10.4+110 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user