Compare commits
	
		
			29 Commits
		
	
	
		
			v0.10.3-be
			...
			v0.10.7-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bbaa42fb01 | ||
|  | 4fe311bc03 | ||
|  | ea68b97ff7 | ||
|  | 6e0f6b528e | ||
|  | a2c227931e | ||
|  | 15ad3bb439 | ||
|  | b03d7fba1a | ||
|  | 31c491d7c5 | ||
|  | 71c80f11f5 | ||
|  | eef4d33431 | ||
|  | d56342e907 | ||
|  | c72c0fdb57 | ||
|  | ffe29009ed | ||
|  | 60e3b68ebd | ||
|  | ee4d0f259f | ||
|  | 0ecfbef0a0 | ||
|  | 1b60e75ca7 | ||
|  | abcfa389e8 | ||
|  | a64bd67ef1 | ||
|  | 4252c2711b | ||
|  | 52913b0450 | ||
|  | 427b0ed8d2 | ||
|  | a85d6d4f08 | ||
|  | 05f712603c | ||
|  | fa2a80e34c | ||
|  | f43e5a2ff1 | ||
|  | b72aa8273e | ||
|  | 520f186e4a | ||
|  | e1e97672cf | 
| @@ -31,4 +31,4 @@ Currently supported App sources: | ||||
|  | ||||
| | <img src="./assets/screenshots/1.apps.png" alt="Apps Page" /> | <img src="./assets/screenshots/2.dark_theme.png" alt="Dark Theme" />           | <img src="./assets/screenshots/3.material_you.png" alt="Material You" />    | | ||||
| | ------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------- | | ||||
| | <img src="./assets/screenshots/4.app.png" alt="App Page" />   | <img src="./assets/screenshots/5.apk_picker.png" alt="Multiple APK Support" /> | <img src="./assets/screenshots/6.apk_install.png" alt="App Installation" /> | | ||||
| | <img src="./assets/screenshots/4.app.png" alt="App Page" />   | <img src="./assets/screenshots/5.app_opts.png" alt="App Options" /> | <img src="./assets/screenshots/6.app_webview.png" alt="App Web View" /> | | ||||
|   | ||||
| @@ -3,7 +3,8 @@ | ||||
|     <application | ||||
|         android:label="Obtainium" | ||||
|         android:name="${applicationName}" | ||||
|         android:icon="@mipmap/ic_launcher"> | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:requestLegacyExternalStorage="true"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:exported="true" | ||||
| @@ -51,7 +52,8 @@ | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
|     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> | ||||
|     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||
|         android:maxSdkVersion="28"/> | ||||
|         android:maxSdkVersion="29"/> | ||||
| </manifest> | ||||
| @@ -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> | ||||
|   | ||||
| Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 234 KiB | 
| Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 238 KiB | 
| Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 140 KiB | 
| Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 139 KiB | 
| Before Width: | Height: | Size: 188 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/screenshots/5.app_opts.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 118 KiB | 
| Before Width: | Height: | Size: 192 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/screenshots/6.app_webview.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 262 KiB | 
| @@ -74,7 +74,6 @@ | ||||
|     "changeX": "Ändern {}", | ||||
|     "installUpdateApps": "Apps installieren/aktualisieren", | ||||
|     "installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren", | ||||
|     "onlyWorksWithNonEVDApps": "Funktioniert nur bei Apps, deren Installationsstatus nicht automatisch erkannt werden kann (ungewöhnlich).", | ||||
|     "markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?", | ||||
|     "no": "Nein", | ||||
|     "yes": "Ja", | ||||
| @@ -178,7 +177,6 @@ | ||||
|     "installedVersionX": "Installierte Version: {}", | ||||
|     "lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}", | ||||
|     "remove": "Entfernen", | ||||
|     "removeAppQuestion": "App entfernen?", | ||||
|     "yesMarkUpdated": "Ja, als aktualisiert markieren", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "App ID oder Name", | ||||
| @@ -211,6 +209,14 @@ | ||||
|     "language": "Sprache", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "App entfernen?", | ||||
|         "other": "App entfernen?" | ||||
|     }, | ||||
|     "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" | ||||
|   | ||||
| @@ -74,7 +74,6 @@ | ||||
|     "changeX": "Change {}", | ||||
|     "installUpdateApps": "Install/Update Apps", | ||||
|     "installUpdateSelectedApps": "Install/Update Selected Apps", | ||||
|     "onlyWorksWithNonEVDApps": "Only works for Apps whose install status cannot be automatically detected (uncommon).", | ||||
|     "markXSelectedAppsAsUpdated": "Mark {} Selected Apps as Updated?", | ||||
|     "no": "No", | ||||
|     "yes": "Yes", | ||||
| @@ -178,7 +177,6 @@ | ||||
|     "installedVersionX": "Installed Version: {}", | ||||
|     "lastUpdateCheckX": "Last Update Check: {}", | ||||
|     "remove": "Remove", | ||||
|     "removeAppQuestion": "Remove App?", | ||||
|     "yesMarkUpdated": "Yes, Mark as Updated", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "App ID or Name", | ||||
| @@ -211,6 +209,14 @@ | ||||
|     "language": "Language", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remove App?", | ||||
|         "other": "Remove Apps?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Too many requests (rate limited) - try again in {} minute", | ||||
|         "other": "Too many requests (rate limited) - try again in {} minutes" | ||||
|   | ||||
| @@ -74,7 +74,6 @@ | ||||
|     "changeX": "Változás {}", | ||||
|     "installUpdateApps": "Appok telepítése/frissítése", | ||||
|     "installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat", | ||||
|     "onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető autom. (nem gyakori).", | ||||
|     "markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?", | ||||
|     "no": "Nem", | ||||
|     "yes": "Igen", | ||||
| @@ -178,7 +177,6 @@ | ||||
|     "installedVersionX": "Telepített verzió: {}", | ||||
|     "lastUpdateCheckX": "Frissítés ellenőrizve: {}", | ||||
|     "remove": "Eltávolítás", | ||||
|     "removeAppQuestion": "Eltávolítja az alkalmazást?", | ||||
|     "yesMarkUpdated": "Igen, megjelölés frissítettként", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "App ID vagy név", | ||||
| @@ -210,6 +208,14 @@ | ||||
|     "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.", | ||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Eltávolítja az alkalmazást?", | ||||
|         "other": "Eltávolítja az alkalmazást?" | ||||
|     }, | ||||
|     "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" | ||||
|   | ||||
| @@ -74,7 +74,6 @@ | ||||
|     "changeX": "Modifica {}", | ||||
|     "installUpdateApps": "Installa/Aggiorna App", | ||||
|     "installUpdateSelectedApps": "Installa/Aggiorna le App selezionate", | ||||
|     "onlyWorksWithNonEVDApps": "Funziona solo per le App il cui stato d'installazione non può essere rilevato automaticamente (inconsueto).", | ||||
|     "markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?", | ||||
|     "no": "No", | ||||
|     "yes": "Sì", | ||||
| @@ -178,7 +177,6 @@ | ||||
|     "installedVersionX": "Versione installata: {}", | ||||
|     "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", | ||||
|     "remove": "Rimuovi", | ||||
|     "removeAppQuestion": "Rimuovere l'App?", | ||||
|     "yesMarkUpdated": "Sì, contrassegna come aggiornato", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "ID o nome dell'App", | ||||
| @@ -211,6 +209,14 @@ | ||||
|     "language": "Lingua", | ||||
|     "storagePermissionDenied": "Storage permission denied", | ||||
|     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Rimuovere l'App?", | ||||
|         "other": "Rimuovere l'App?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", | ||||
|         "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|     "appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません", | ||||
|     "functionNotImplemented": "このクラスはこの機能を実装していません", | ||||
|     "placeholder": "プレースホルダー", | ||||
|     "someErrors": "いくつかのエラーが発生しました", | ||||
|     "someErrors": "何らかのエラーが発生しました", | ||||
|     "unexpectedError": "予期せぬエラーが発生しました", | ||||
|     "ok": "OK", | ||||
|     "and": "と", | ||||
| @@ -74,7 +74,6 @@ | ||||
|     "changeX": "{} を変更する", | ||||
|     "installUpdateApps": "アプリのインストール/アップデート", | ||||
|     "installUpdateSelectedApps": "選択したアプリのインストール/アップデート", | ||||
|     "onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。", | ||||
|     "markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?", | ||||
|     "no": "いいえ", | ||||
|     "yes": "はい", | ||||
| @@ -82,7 +81,7 @@ | ||||
|     "pinToTop": "トップに固定", | ||||
|     "unpinFromTop": "トップから固定解除", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?", | ||||
|     "installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗するなどして、Obtainiumに表示されるアプリのバージョンが正しくない場合に役立ちます。", | ||||
|     "installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗した場合など、Obtainiumに表示されるアプリのバージョンが正しくない場合に有効です。", | ||||
|     "shareSelectedAppURLs": "選択したアプリのURLを共有する", | ||||
|     "resetInstallStatus": "インストール状態をリセットする", | ||||
|     "more": "もっと見る", | ||||
| @@ -109,7 +108,7 @@ | ||||
|     "searchX": "{}で検索", | ||||
|     "noResults": "結果は見つかりませんでした", | ||||
|     "importX": "{}をインポートする", | ||||
|     "importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティーのインポートメソッドにのみ影響します。", | ||||
|     "importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティのインポートメソッドにのみ影響します。", | ||||
|     "importErrors": "インポートエラー", | ||||
|     "importedXOfYApps": "{} / {} アプリをインポートしました", | ||||
|     "followingURLsHadErrors": "以下のURLでエラーが発生しました:", | ||||
| @@ -133,7 +132,7 @@ | ||||
|     "bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔", | ||||
|     "neverManualOnly": "手動", | ||||
|     "appearance": "外観", | ||||
|     "showWebInAppView": "アプリビューにソースウェブページを表示する", | ||||
|     "showWebInAppView": "アプリページにソースのWebページを表示する", | ||||
|     "pinUpdates": "アップデートがあるアプリをトップに固定する", | ||||
|     "updates": "アップデート", | ||||
|     "sourceSpecific": "Github アクセストークン", | ||||
| @@ -178,13 +177,12 @@ | ||||
|     "installedVersionX": "インストールされたバージョン: {}", | ||||
|     "lastUpdateCheckX": "最終アップデート確認: {}", | ||||
|     "remove": "削除", | ||||
|     "removeAppQuestion": "アプリを削除しますか?", | ||||
|     "yesMarkUpdated": "はい、アップデート済みとしてマークします", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "アプリのIDまたは名前", | ||||
|     "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした", | ||||
|     "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Third-Party Repo", | ||||
|     "fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ", | ||||
|     "steam": "Steam", | ||||
|     "steamMobile": "Steam Mobile", | ||||
|     "steamChat": "Steam Chat", | ||||
| @@ -211,6 +209,14 @@ | ||||
|     "language": "言語", | ||||
|     "storagePermissionDenied": "ストレージ権限が拒否されました", | ||||
|     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", | ||||
|     "filterAPKsByRegEx": "正規表現でAPKを絞り込む", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Only works for Apps with version detection disabled.", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "アプリを削除しますか?", | ||||
|         "other": "アプリを削除しますか?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||
|   | ||||
| @@ -178,7 +178,6 @@ | ||||
|     "installedVersionX": "已安装: {}", | ||||
|     "lastUpdateCheckX": "最后检查: {}", | ||||
|     "remove": "删除", | ||||
|     "removeAppQuestion": "删除应用?", | ||||
|     "yesMarkUpdated": "'是的,标为已更新", | ||||
|     "fdroid": "F-Droid", | ||||
|     "appIdOrName": "应用 ID 或名称", | ||||
| @@ -211,6 +210,13 @@ | ||||
|     "language": "语言", | ||||
|     "storagePermissionDenied": "存储权限已被拒绝", | ||||
|     "selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别", | ||||
|     "filterAPKsByRegEx": "Filter APKs by Regular Expression", | ||||
|     "removeFromObtainium": "Remove from Obtainium", | ||||
|     "uninstallFromDevice": "Uninstall from Device", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "删除应用?", | ||||
|         "other": "删除应用?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", | ||||
|         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" | ||||
|   | ||||
| @@ -26,15 +26,7 @@ class Codeberg extends AppSource { | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 if (value == null || value.isEmpty) { | ||||
|                   return null; | ||||
|                 } | ||||
|                 try { | ||||
|                   RegExp(value); | ||||
|                 } catch (e) { | ||||
|                   return tr('invalidRegEx'); | ||||
|                 } | ||||
|                 return null; | ||||
|                 return regExValidator(value); | ||||
|               } | ||||
|             ]) | ||||
|       ] | ||||
| @@ -72,7 +64,7 @@ class Codeberg extends AppSource { | ||||
|             ? additionalSettings['filterReleaseTitlesByRegEx'] | ||||
|             : null; | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases')); | ||||
|         'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var releases = jsonDecode(res.body) as List<dynamic>; | ||||
|  | ||||
|   | ||||
| @@ -65,15 +65,7 @@ class GitHub extends AppSource { | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 if (value == null || value.isEmpty) { | ||||
|                   return null; | ||||
|                 } | ||||
|                 try { | ||||
|                   RegExp(value); | ||||
|                 } catch (e) { | ||||
|                   return tr('invalidRegEx'); | ||||
|                 } | ||||
|                 return null; | ||||
|                 return regExValidator(value); | ||||
|               } | ||||
|             ]) | ||||
|       ] | ||||
| @@ -119,7 +111,7 @@ class GitHub extends AppSource { | ||||
|             ? additionalSettings['filterReleaseTitlesByRegEx'] | ||||
|             : null; | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases')); | ||||
|         'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var releases = jsonDecode(res.body) as List<dynamic>; | ||||
|  | ||||
|   | ||||
| @@ -150,6 +150,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|   Map<String, dynamic> values = {}; | ||||
|   late List<List<Widget>> formInputs; | ||||
|   List<List<Widget>> rows = []; | ||||
|   String? initKey; | ||||
|  | ||||
|   // If any value changes, call this to update the parent with value and validity | ||||
|   void someValueChanged({bool isBuilding = false}) { | ||||
| @@ -169,13 +170,10 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|     widget.onValueChanges(returnValues, valid, isBuilding); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|   initForm() { | ||||
|     initKey = widget.key.toString(); | ||||
|     // Initialize form values as all empty | ||||
|     values.clear(); | ||||
|     int j = 0; | ||||
|     for (var row in widget.items) { | ||||
|       for (var e in row) { | ||||
|         values[e.key] = e.defaultValue; | ||||
| @@ -245,8 +243,17 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|     someValueChanged(isBuilding: true); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     initForm(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (widget.key.toString() != initKey) { | ||||
|       initForm(); | ||||
|     } | ||||
|     for (var r = 0; r < formInputs.length; r++) { | ||||
|       for (var e = 0; e < formInputs[r].length; e++) { | ||||
|         if (widget.items[r][e] is GeneratedFormSwitch) { | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class NoReleasesError extends ObtainiumError { | ||||
| } | ||||
|  | ||||
| class NoAPKError extends ObtainiumError { | ||||
|   NoAPKError() : super(tr('noReleaseFound')); | ||||
|   NoAPKError() : super(tr('noAPKFound')); | ||||
| } | ||||
|  | ||||
| class NoVersionError 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.10.3'; | ||||
| const String currentVersion = '0.10.7'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|   Map<String, dynamic> additionalSettings = {}; | ||||
|   bool additionalSettingsValid = true; | ||||
|   List<String> pickedCategories = []; | ||||
|   int searchnum = 0; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -40,10 +41,14 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|  | ||||
|     bool doingSomething = gettingAppInfo || searching; | ||||
|  | ||||
|     changeUserInput(String input, bool valid, bool isBuilding) { | ||||
|     changeUserInput(String input, bool valid, bool isBuilding, | ||||
|         {bool isSearch = false}) { | ||||
|       userInput = input; | ||||
|       if (!isBuilding) { | ||||
|         setState(() { | ||||
|           if (isSearch) { | ||||
|             searchnum++; | ||||
|           } | ||||
|           var source = valid ? sourceProvider.getSource(userInput) : null; | ||||
|           if (pickedSource.runtimeType != source.runtimeType) { | ||||
|             pickedSource = source; | ||||
| @@ -70,6 +75,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|             additionalSettings['noVersionDetection'] == true; | ||||
|         var cont = true; | ||||
|         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && | ||||
|             // ignore: use_build_context_synchronously | ||||
|             await showDialog( | ||||
|                     context: context, | ||||
|                     builder: (BuildContext ctx) { | ||||
| @@ -88,6 +94,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           cont = false; | ||||
|         } | ||||
|         if (userPickedNoVersionDetection && | ||||
|             // ignore: use_build_context_synchronously | ||||
|             await showDialog( | ||||
|                     context: context, | ||||
|                     builder: (BuildContext ctx) { | ||||
| @@ -167,10 +174,12 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                               child: GeneratedForm( | ||||
|                                   key: Key(searchnum.toString()), | ||||
|                                   items: [ | ||||
|                                     [ | ||||
|                                       GeneratedFormTextField('appSourceURL', | ||||
|                                           label: tr('appSourceURL'), | ||||
|                                           defaultValue: userInput, | ||||
|                                           additionalValidators: [ | ||||
|                                             (value) { | ||||
|                                               try { | ||||
| @@ -294,8 +303,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                           if (selectedUrls != null && | ||||
|                                               selectedUrls.isNotEmpty) { | ||||
|                                             changeUserInput( | ||||
|                                                 selectedUrls[0], true, false); | ||||
|                                             addApp(resetUserInputAfter: true); | ||||
|                                                 selectedUrls[0], true, false, | ||||
|                                                 isSearch: true); | ||||
|                                           } | ||||
|                                         }).catchError((e) { | ||||
|                                           showError(e, context); | ||||
| @@ -325,6 +334,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                               height: 16, | ||||
|                             ), | ||||
|                             GeneratedForm( | ||||
|                                 key: Key(pickedSource.runtimeType.toString()), | ||||
|                                 items: pickedSource! | ||||
|                                     .combinedAppSpecificSettingFormItems, | ||||
|                                 onValueChanges: (values, valid, isBuilding) { | ||||
|   | ||||
| @@ -42,6 +42,8 @@ class _AppPageState extends State<AppPage> { | ||||
|       getUpdate(app.app.id); | ||||
|     } | ||||
|     var trackOnly = app?.app.additionalSettings['trackOnly'] == true; | ||||
|     var noVersionDetection = | ||||
|         app?.app.additionalSettings['noVersionDetection'] == true; | ||||
|  | ||||
|     var infoColumn = Column( | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
| @@ -190,8 +192,9 @@ class _AppPageState extends State<AppPage> { | ||||
|                   child: Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||
|                       children: [ | ||||
|                         if (app?.app.installedVersion != null && | ||||
|                         if (noVersionDetection && | ||||
|                             !trackOnly && | ||||
|                             app?.app.installedVersion != null && | ||||
|                             app?.app.installedVersion != app?.app.latestVersion) | ||||
|                           IconButton( | ||||
|                               onPressed: app?.downloadProgress != null | ||||
| @@ -203,13 +206,6 @@ class _AppPageState extends State<AppPage> { | ||||
|                                             return AlertDialog( | ||||
|                                               title: Text(tr( | ||||
|                                                   'alreadyUpToDateQuestion')), | ||||
|                                               content: Text( | ||||
|                                                   tr('onlyWorksWithNonEVDApps'), | ||||
|                                                   style: const TextStyle( | ||||
|                                                       fontWeight: | ||||
|                                                           FontWeight.bold, | ||||
|                                                       fontStyle: | ||||
|                                                           FontStyle.italic)), | ||||
|                                               actions: [ | ||||
|                                                 TextButton( | ||||
|                                                     onPressed: () { | ||||
| @@ -268,7 +264,8 @@ class _AppPageState extends State<AppPage> { | ||||
|                                             }).toList(); | ||||
|                                             return GeneratedFormModal( | ||||
|                                               title: tr('additionalOptions'), | ||||
|                                                 items: items); | ||||
|                                               items: items, | ||||
|                                             ); | ||||
|                                           }).then((values) { | ||||
|                                         if (app != null && values != null) { | ||||
|                                           var changedApp = app.app; | ||||
| @@ -289,7 +286,15 @@ class _AppPageState extends State<AppPage> { | ||||
|                                       }); | ||||
|                                     }, | ||||
|                               tooltip: tr('additionalOptions'), | ||||
|                               icon: const Icon(Icons.settings)), | ||||
|                               icon: const Icon(Icons.edit)), | ||||
|                         if (app != null && app.installedInfo != null) | ||||
|                           IconButton( | ||||
|                             onPressed: () { | ||||
|                               appsProvider.openAppSettings(app.app.id); | ||||
|                             }, | ||||
|                             icon: const Icon(Icons.settings), | ||||
|                             tooltip: tr('settings'), | ||||
|                           ), | ||||
|                         if (app != null && settingsProvider.showAppWebpage) | ||||
|                           IconButton( | ||||
|                               onPressed: () { | ||||
| @@ -317,7 +322,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                               tooltip: tr('more')), | ||||
|                         const SizedBox(width: 16.0), | ||||
|                         Expanded( | ||||
|                             child: ElevatedButton( | ||||
|                             child: TextButton( | ||||
|                                 onPressed: (app?.app.installedVersion == null || | ||||
|                                             app?.app.installedVersion != | ||||
|                                                 app?.app.latestVersion) && | ||||
| @@ -356,43 +361,16 @@ class _AppPageState extends State<AppPage> { | ||||
|                                         ? tr('update') | ||||
|                                         : tr('markUpdated')))), | ||||
|                         const SizedBox(width: 16.0), | ||||
|                         ElevatedButton( | ||||
|                         Expanded( | ||||
|                             child: TextButton( | ||||
|                           onPressed: app?.downloadProgress != null | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   showDialog( | ||||
|                                       context: context, | ||||
|                                       builder: (BuildContext ctx) { | ||||
|                                         return AlertDialog( | ||||
|                                           title: Text(tr('removeAppQuestion')), | ||||
|                                           content: Text(tr( | ||||
|                                               'xWillBeRemovedButRemainInstalled', | ||||
|                                               args: [ | ||||
|                                                 app?.installedInfo?.name ?? | ||||
|                                                     app?.app.name ?? | ||||
|                                                     tr('app') | ||||
|                                               ])), | ||||
|                                           actions: [ | ||||
|                                             TextButton( | ||||
|                                                 onPressed: () { | ||||
|                                                   HapticFeedback | ||||
|                                                       .selectionClick(); | ||||
|                                                   appsProvider.removeApps( | ||||
|                                                       [app!.app.id]).then((_) { | ||||
|                                                     int count = 0; | ||||
|                                                     Navigator.of(context) | ||||
|                                                         .popUntil((_) => | ||||
|                                                             count++ >= 2); | ||||
|                                                   }); | ||||
|                                                 }, | ||||
|                                                 child: Text(tr('remove'))), | ||||
|                                             TextButton( | ||||
|                                                 onPressed: () { | ||||
|                                   appsProvider.removeAppsWithModal( | ||||
|                                       context, [app!.app]).then((value) { | ||||
|                                     if (value == true) { | ||||
|                                       Navigator.of(context).pop(); | ||||
|                                                 }, | ||||
|                                                 child: Text(tr('cancel'))) | ||||
|                                           ], | ||||
|                                         ); | ||||
|                                     } | ||||
|                                   }); | ||||
|                                 }, | ||||
|                           style: TextButton.styleFrom( | ||||
| @@ -401,7 +379,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                               surfaceTintColor: | ||||
|                                   Theme.of(context).colorScheme.error), | ||||
|                           child: Text(tr('remove')), | ||||
|                         ), | ||||
|                         )), | ||||
|                       ])), | ||||
|               if (app?.downloadProgress != null) | ||||
|                 Padding( | ||||
| @@ -413,3 +391,18 @@ class _AppPageState extends State<AppPage> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class RemoveAppsModal extends StatefulWidget { | ||||
|   const RemoveAppsModal({super.key, this.apps = const []}); | ||||
|   final List<App> apps; | ||||
|  | ||||
|   @override | ||||
|   State<RemoveAppsModal> createState() => _RemoveAppsModalState(); | ||||
| } | ||||
|  | ||||
| class _RemoveAppsModalState extends State<RemoveAppsModal> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return const Placeholder(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -344,13 +344,15 @@ class AppsPageState extends State<AppsPage> { | ||||
|                   )); | ||||
|             }, childCount: sortedApps.length)) | ||||
|           ])), | ||||
|       persistentFooterButtons: [ | ||||
|       persistentFooterButtons: appsProvider.apps.isEmpty | ||||
|           ? null | ||||
|           : [ | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   selectedApps.isEmpty | ||||
|                       ? TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                           style: const ButtonStyle( | ||||
|                               visualDensity: VisualDensity.compact), | ||||
|                           onPressed: () { | ||||
|                             selectThese(sortedApps.map((e) => e.app).toList()); | ||||
|                           }, | ||||
| @@ -360,11 +362,12 @@ class AppsPageState extends State<AppsPage> { | ||||
|                           ), | ||||
|                           label: Text(sortedApps.length.toString())) | ||||
|                       : TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                           style: const ButtonStyle( | ||||
|                               visualDensity: VisualDensity.compact), | ||||
|                           onPressed: () { | ||||
|                             selectedApps.isEmpty | ||||
|                           ? selectThese(sortedApps.map((e) => e.app).toList()) | ||||
|                                 ? selectThese( | ||||
|                                     sortedApps.map((e) => e.app).toList()) | ||||
|                                 : clearSelected(); | ||||
|                           }, | ||||
|                           icon: Icon( | ||||
| @@ -386,42 +389,49 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                 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()); | ||||
|                                     } | ||||
|                                   }); | ||||
|                                         appsProvider.removeAppsWithModal( | ||||
|                                             context, selectedApps.toList()); | ||||
|                                         // 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) | ||||
|                                   onPressed: appsProvider | ||||
|                                               .areDownloadsRunning() || | ||||
|                                           (existingUpdateIdsAllOrSelected | ||||
|                                                   .isEmpty && | ||||
|                                               newInstallIdsAllOrSelected | ||||
|                                                   .isEmpty && | ||||
|                                               trackOnlyUpdateIdsAllOrSelected | ||||
|                                                   .isEmpty) | ||||
|                                       ? null | ||||
|                                       : () { | ||||
|                                           HapticFeedback.heavyImpact(); | ||||
|                                     List<GeneratedFormItem> formItems = []; | ||||
|                                           List<GeneratedFormItem> formItems = | ||||
|                                               []; | ||||
|                                           if (existingUpdateIdsAllOrSelected | ||||
|                                               .isNotEmpty) { | ||||
|                                             formItems.add(GeneratedFormSwitch( | ||||
| @@ -434,7 +444,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                 ]), | ||||
|                                                 defaultValue: true)); | ||||
|                                           } | ||||
|                                     if (newInstallIdsAllOrSelected.isNotEmpty) { | ||||
|                                           if (newInstallIdsAllOrSelected | ||||
|                                               .isNotEmpty) { | ||||
|                                             formItems.add(GeneratedFormSwitch( | ||||
|                                                 'installs', | ||||
|                                                 label: tr('installX', args: [ | ||||
| @@ -451,7 +462,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                               .isNotEmpty) { | ||||
|                                             formItems.add(GeneratedFormSwitch( | ||||
|                                                 'trackonlies', | ||||
|                                           label: tr('markXTrackOnlyAsUpdated', | ||||
|                                                 label: tr( | ||||
|                                                     'markXTrackOnlyAsUpdated', | ||||
|                                                     args: [ | ||||
|                                                       plural( | ||||
|                                                           'apps', | ||||
| @@ -467,8 +479,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                           showDialog<Map<String, dynamic>?>( | ||||
|                                               context: context, | ||||
|                                               builder: (BuildContext ctx) { | ||||
|                                           var totalApps = | ||||
|                                               existingUpdateIdsAllOrSelected.length + | ||||
|                                                 var totalApps = existingUpdateIdsAllOrSelected | ||||
|                                                         .length + | ||||
|                                                     newInstallIdsAllOrSelected | ||||
|                                                         .length + | ||||
|                                                     trackOnlyUpdateIdsAllOrSelected | ||||
| @@ -560,7 +572,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                             cont = await showDialog< | ||||
|                                                         Map<String, dynamic>?>( | ||||
|                                                     context: context, | ||||
|                                               builder: (BuildContext ctx) { | ||||
|                                                     builder: | ||||
|                                                         (BuildContext ctx) { | ||||
|                                                       return GeneratedFormModal( | ||||
|                                                         title: tr('categorize'), | ||||
|                                                         items: const [], | ||||
| @@ -572,7 +585,9 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                 null; | ||||
|                                           } | ||||
|                                           if (cont) { | ||||
|                                       await showDialog<Map<String, dynamic>?>( | ||||
|                                             // ignore: use_build_context_synchronously | ||||
|                                             await showDialog< | ||||
|                                                     Map<String, dynamic>?>( | ||||
|                                                 context: context, | ||||
|                                                 builder: (BuildContext ctx) { | ||||
|                                                   return GeneratedFormModal( | ||||
| @@ -586,11 +601,15 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                         preselected: !showPrompt | ||||
|                                                             ? preselected ?? {} | ||||
|                                                             : {}, | ||||
|                                                   showLabelWhenNotEmpty: false, | ||||
|                                                   onSelected: (categories) { | ||||
|                                                         showLabelWhenNotEmpty: | ||||
|                                                             false, | ||||
|                                                         onSelected: | ||||
|                                                             (categories) { | ||||
|                                                           appsProvider.saveApps( | ||||
|                                                         selectedApps.map((e) { | ||||
|                                                       e.categories = categories; | ||||
|                                                               selectedApps | ||||
|                                                                   .map((e) { | ||||
|                                                             e.categories = | ||||
|                                                                 categories; | ||||
|                                                             return e; | ||||
|                                                           }).toList()); | ||||
|                                                         }, | ||||
| @@ -618,7 +637,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                 scrollable: true, | ||||
|                                                 content: Padding( | ||||
|                                                   padding: | ||||
|                                                 const EdgeInsets.only(top: 6), | ||||
|                                                       const EdgeInsets.only( | ||||
|                                                           top: 6), | ||||
|                                                   child: Row( | ||||
|                                                       mainAxisAlignment: | ||||
|                                                           MainAxisAlignment | ||||
| @@ -636,33 +656,26 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                                             (BuildContext | ||||
|                                                                                 ctx) { | ||||
|                                                                           return AlertDialog( | ||||
|                                                                       title: Text(tr( | ||||
|                                                                           'markXSelectedAppsAsUpdated', | ||||
|                                                                           args: [ | ||||
|                                                                             title: | ||||
|                                                                                 Text(tr('markXSelectedAppsAsUpdated', args: [ | ||||
|                                                                               selectedApps.length.toString() | ||||
|                                                                             ])), | ||||
|                                                                             content: | ||||
|                                                                                 Text( | ||||
|                                                                         tr('onlyWorksWithNonEVDApps'), | ||||
|                                                                         style: const TextStyle( | ||||
|                                                                             fontWeight: | ||||
|                                                                                 FontWeight.bold, | ||||
|                                                                             fontStyle: FontStyle.italic), | ||||
|                                                                               tr('onlyWorksWithNonVersionDetectApps'), | ||||
|                                                                               style: const TextStyle(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), | ||||
|                                                                             ), | ||||
|                                                                             actions: [ | ||||
|                                                                               TextButton( | ||||
|                                                                             onPressed: | ||||
|                                                                                 () { | ||||
|                                                                                   onPressed: () { | ||||
|                                                                                     Navigator.of(context).pop(); | ||||
|                                                                                   }, | ||||
|                                                                             child: | ||||
|                                                                                 Text(tr('no'))), | ||||
|                                                                                   child: Text(tr('no'))), | ||||
|                                                                               TextButton( | ||||
|                                                                             onPressed: | ||||
|                                                                                 () { | ||||
|                                                                                   onPressed: () { | ||||
|                                                                                     HapticFeedback.selectionClick(); | ||||
|                                                                                     appsProvider.saveApps(selectedApps.map((a) { | ||||
|                                                                                 if (a.installedVersion != null) { | ||||
|                                                                                       if (a.installedVersion != null && a.additionalSettings['noVersionDetection'] == true) { | ||||
|                                                                                         a.installedVersion = a.latestVersion; | ||||
|                                                                                       } | ||||
|                                                                                       return a; | ||||
| @@ -670,8 +683,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|  | ||||
|                                                                                     Navigator.of(context).pop(); | ||||
|                                                                                   }, | ||||
|                                                                             child: | ||||
|                                                                                 Text(tr('yes'))) | ||||
|                                                                                   child: Text(tr('yes'))) | ||||
|                                                                             ], | ||||
|                                                                           ); | ||||
|                                                                         }).whenComplete(() { | ||||
| @@ -686,29 +698,36 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                                 Icons.done)), | ||||
|                                                         IconButton( | ||||
|                                                           onPressed: () { | ||||
|                                                       var pinStatus = | ||||
|                                                           selectedApps | ||||
|                                                             var pinStatus = selectedApps | ||||
|                                                                 .where((element) => | ||||
|                                                                     element | ||||
|                                                                         .pinned) | ||||
|                                                                 .isEmpty; | ||||
|                                                       appsProvider.saveApps( | ||||
|                                                           selectedApps.map((e) { | ||||
|                                                         e.pinned = pinStatus; | ||||
|                                                             appsProvider | ||||
|                                                                 .saveApps( | ||||
|                                                                     selectedApps | ||||
|                                                                         .map( | ||||
|                                                                             (e) { | ||||
|                                                               e.pinned = | ||||
|                                                                   pinStatus; | ||||
|                                                               return e; | ||||
|                                                             }).toList()); | ||||
|                                                       Navigator.of(context) | ||||
|                                                             Navigator.of( | ||||
|                                                                     context) | ||||
|                                                                 .pop(); | ||||
|                                                           }, | ||||
|                                                           tooltip: selectedApps | ||||
|                                                                   .where((element) => | ||||
|                                                                 element.pinned) | ||||
|                                                                       element | ||||
|                                                                           .pinned) | ||||
|                                                                   .isEmpty | ||||
|                                                               ? tr('pinToTop') | ||||
|                                                         : tr('unpinFromTop'), | ||||
|                                                               : tr( | ||||
|                                                                   'unpinFromTop'), | ||||
|                                                           icon: Icon(selectedApps | ||||
|                                                                   .where((element) => | ||||
|                                                                 element.pinned) | ||||
|                                                                       element | ||||
|                                                                           .pinned) | ||||
|                                                                   .isEmpty | ||||
|                                                               ? Icons | ||||
|                                                                   .bookmark_outline_rounded | ||||
| @@ -720,53 +739,63 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                             String urls = ''; | ||||
|                                                             for (var a | ||||
|                                                                 in selectedApps) { | ||||
|                                                         urls += '${a.url}\n'; | ||||
|                                                               urls += | ||||
|                                                                   '${a.url}\n'; | ||||
|                                                             } | ||||
|                                                       urls = urls.substring( | ||||
|                                                           0, urls.length - 1); | ||||
|                                                             urls = | ||||
|                                                                 urls.substring( | ||||
|                                                                     0, | ||||
|                                                                     urls.length - | ||||
|                                                                         1); | ||||
|                                                             Share.share(urls, | ||||
|                                                                 subject: tr( | ||||
|                                                                     'selectedAppURLsFromObtainium')); | ||||
|                                                       Navigator.of(context) | ||||
|                                                             Navigator.of( | ||||
|                                                                     context) | ||||
|                                                                 .pop(); | ||||
|                                                           }, | ||||
|                                                           tooltip: tr( | ||||
|                                                               'shareSelectedAppURLs'), | ||||
|                                                     icon: | ||||
|                                                         const Icon(Icons.share), | ||||
|                                                           icon: const Icon( | ||||
|                                                               Icons.share), | ||||
|                                                         ), | ||||
|                                                         IconButton( | ||||
|                                                           onPressed: () { | ||||
|                                                             showDialog( | ||||
|                                                           context: context, | ||||
|                                                           builder: (BuildContext | ||||
|                                                                 context: | ||||
|                                                                     context, | ||||
|                                                                 builder: | ||||
|                                                                     (BuildContext | ||||
|                                                                         ctx) { | ||||
|                                                                   return GeneratedFormModal( | ||||
|                                                                     title: tr( | ||||
|                                                                         'resetInstallStatusForSelectedAppsQuestion'), | ||||
|                                                                     items: const [], | ||||
|                                                               initValid: true, | ||||
|                                                                     initValid: | ||||
|                                                                         true, | ||||
|                                                                     message: tr( | ||||
|                                                                         'installStatusOfXWillBeResetExplanation', | ||||
|                                                                         args: [ | ||||
|                                                                           plural( | ||||
|                                                                               'app', | ||||
|                                                                         selectedApps | ||||
|                                                                             .length) | ||||
|                                                                               selectedApps.length) | ||||
|                                                                         ]), | ||||
|                                                                   ); | ||||
|                                                                 }).then((values) { | ||||
|                                                         if (values != null) { | ||||
|                                                               if (values != | ||||
|                                                                   null) { | ||||
|                                                                 appsProvider.saveApps( | ||||
|                                                                     selectedApps | ||||
|                                                                   .map((e) { | ||||
|                                                                         .map( | ||||
|                                                                             (e) { | ||||
|                                                                   e.installedVersion = | ||||
|                                                                       null; | ||||
|                                                                   return e; | ||||
|                                                                 }).toList()); | ||||
|                                                               } | ||||
|                                                             }).whenComplete(() { | ||||
|                                                         Navigator.of(context) | ||||
|                                                               Navigator.of( | ||||
|                                                                       context) | ||||
|                                                                   .pop(); | ||||
|                                                             }); | ||||
|                                                           }, | ||||
| @@ -807,11 +836,9 @@ class AppsPageState extends State<AppsPage> { | ||||
|                       color: Theme.of(context).colorScheme.primary, | ||||
|                     ), | ||||
|                   ), | ||||
|             appsProvider.apps.isEmpty | ||||
|                 ? const SizedBox() | ||||
|                 : TextButton.icon( | ||||
|                     style: | ||||
|                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||
|                   TextButton.icon( | ||||
|                       style: const ButtonStyle( | ||||
|                           visualDensity: VisualDensity.compact), | ||||
|                       label: Text( | ||||
|                         filter.isIdenticalTo(neutralFilter, settingsProvider) | ||||
|                             ? tr('filter') | ||||
| @@ -859,7 +886,8 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                   CategoryEditorSelector( | ||||
|                                     preselected: filter.categoryFilter, | ||||
|                                     onSelected: (categories) { | ||||
|                                     filter.categoryFilter = categories.toSet(); | ||||
|                                       filter.categoryFilter = | ||||
|                                           categories.toSet(); | ||||
|                                     }, | ||||
|                                   ) | ||||
|                                 ], | ||||
|   | ||||
| @@ -63,21 +63,29 @@ class _HomePageState extends State<HomePage> { | ||||
|                 .map((e) => | ||||
|                     NavigationDestination(icon: Icon(e.icon), label: e.title)) | ||||
|                 .toList(), | ||||
|             onDestinationSelected: (int index) { | ||||
|             onDestinationSelected: (int index) async { | ||||
|               HapticFeedback.selectionClick(); | ||||
|               setState(() { | ||||
|               if (index == 0) { | ||||
|                 while ((pages[0].widget.key as GlobalKey<AppsPageState>) | ||||
|                         .currentState != | ||||
|                     null) { | ||||
|                   // Avoid duplicate GlobalKey error | ||||
|                   await Future.delayed(const Duration(microseconds: 1)); | ||||
|                 } | ||||
|                 setState(() { | ||||
|                   selectedIndexHistory.clear(); | ||||
|                 }); | ||||
|               } else if (selectedIndexHistory.isEmpty || | ||||
|                   (selectedIndexHistory.isNotEmpty && | ||||
|                       selectedIndexHistory.last != index)) { | ||||
|                 setState(() { | ||||
|                   int existingInd = selectedIndexHistory.indexOf(index); | ||||
|                   if (existingInd >= 0) { | ||||
|                     selectedIndexHistory.removeAt(existingInd); | ||||
|                   } | ||||
|                   selectedIndexHistory.add(index); | ||||
|                 } | ||||
|                 }); | ||||
|               } | ||||
|             }, | ||||
|             selectedIndex: | ||||
|                 selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last, | ||||
|   | ||||
| @@ -564,10 +564,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> { | ||||
|           widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')), | ||||
|       content: Column(children: [ | ||||
|         ...urlWithDescriptionSelections.keys.map((urlWithD) { | ||||
|           return Row(children: [ | ||||
|             Checkbox( | ||||
|                 value: urlWithDescriptionSelections[urlWithD], | ||||
|                 onChanged: (value) { | ||||
|           select(bool? value) { | ||||
|             setState(() { | ||||
|               value ??= false; | ||||
|               if (value! && widget.onlyOneSelectionAllowed) { | ||||
| @@ -576,6 +573,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> { | ||||
|                 urlWithDescriptionSelections[urlWithD] = value!; | ||||
|               } | ||||
|             }); | ||||
|           } | ||||
|  | ||||
|           return Row(children: [ | ||||
|             Checkbox( | ||||
|                 value: urlWithDescriptionSelections[urlWithD], | ||||
|                 onChanged: (value) { | ||||
|                   select(value); | ||||
|                 }), | ||||
|             const SizedBox( | ||||
|               width: 8, | ||||
| @@ -599,13 +603,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> { | ||||
|                           const TextStyle(decoration: TextDecoration.underline), | ||||
|                       textAlign: TextAlign.start, | ||||
|                     )), | ||||
|                 Text( | ||||
|                 GestureDetector( | ||||
|                   onTap: () { | ||||
|                     select(!(urlWithDescriptionSelections[urlWithD] ?? false)); | ||||
|                   }, | ||||
|                   child: Text( | ||||
|                     urlWithD.value.length > 128 | ||||
|                         ? '${urlWithD.value.substring(0, 128)}...' | ||||
|                         : urlWithD.value, | ||||
|                     style: const TextStyle( | ||||
|                         fontStyle: FontStyle.italic, fontSize: 12), | ||||
|                   ), | ||||
|                 ), | ||||
|                 const SizedBox( | ||||
|                   height: 8, | ||||
|                 ) | ||||
|   | ||||
| @@ -4,10 +4,8 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:obtainium/components/custom_app_bar.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/logs_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:android_intent_plus/flag.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -12,6 +13,8 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:install_plugin_v2/install_plugin_v2.dart'; | ||||
| import 'package:installed_apps/app_info.dart'; | ||||
| import 'package:installed_apps/installed_apps.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/logs_provider.dart'; | ||||
| import 'package:obtainium/providers/notifications_provider.dart'; | ||||
| @@ -23,6 +26,7 @@ import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:flutter_fgbg/flutter_fgbg.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:android_intent_plus/android_intent.dart'; | ||||
|  | ||||
| class AppInMemory { | ||||
|   late App app; | ||||
| @@ -247,7 +251,11 @@ class AppsProvider with ChangeNotifier { | ||||
|         !(await canDowngradeApps())) { | ||||
|       throw DowngradeError(); | ||||
|     } | ||||
|     await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium'); | ||||
|     await InstallPlugin.installApk(file.file.path, obtainiumId); | ||||
|     if (file.appId == obtainiumId) { | ||||
|       // Obtainium prompt should be lowest | ||||
|       await Future.delayed(const Duration(milliseconds: 500)); | ||||
|     } | ||||
|     apps[file.appId]!.app.installedVersion = | ||||
|         apps[file.appId]!.app.latestVersion; | ||||
|     // Don't correct install status as installation may not be done yet | ||||
| @@ -255,6 +263,15 @@ class AppsProvider with ChangeNotifier { | ||||
|         attemptToCorrectInstallStatus: false); | ||||
|   } | ||||
|  | ||||
|   void uninstallApp(String appId) async { | ||||
|     var intent = AndroidIntent( | ||||
|         action: 'android.intent.action.DELETE', | ||||
|         data: 'package:$appId', | ||||
|         flags: <int>[Flag.FLAG_ACTIVITY_NEW_TASK], | ||||
|         package: 'vnd.android.package-archive'); | ||||
|     await intent.launch(); | ||||
|   } | ||||
|  | ||||
|   Future<String?> confirmApkUrl(App app, BuildContext? context) async { | ||||
|     // If the App has more than one APK, the user should pick one (if context provided) | ||||
|     String? apkUrl = app.apkUrls[app.preferredApkIndex]; | ||||
| @@ -262,6 +279,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis; | ||||
|  | ||||
|     if (app.apkUrls.length > 1 && context != null) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       apkUrl = await showDialog( | ||||
|           context: context, | ||||
|           builder: (BuildContext ctx) { | ||||
| @@ -281,6 +299,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     if (apkUrl != null && | ||||
|         getHost(apkUrl) != getHost(app.url) && | ||||
|         context != null) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       if (await showDialog( | ||||
|               context: context, | ||||
|               builder: (BuildContext ctx) { | ||||
| @@ -438,9 +457,6 @@ class AppsProvider with ChangeNotifier { | ||||
|     } catch (e) { | ||||
|       // | ||||
|     } | ||||
|     if (!res) { | ||||
|       logs.add(tr('versionCorrectionDisabled')); | ||||
|     } | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
| @@ -619,6 +635,57 @@ class AppsProvider with ChangeNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> removeAppsWithModal(BuildContext context, List<App> apps) async { | ||||
|     var showUninstallOption = | ||||
|         apps.where((a) => a.installedVersion != null).isNotEmpty; | ||||
|     var values = await showDialog( | ||||
|         context: context, | ||||
|         builder: (BuildContext ctx) { | ||||
|           return GeneratedFormModal( | ||||
|             title: plural('removeAppQuestion', apps.length), | ||||
|             items: !showUninstallOption | ||||
|                 ? [] | ||||
|                 : [ | ||||
|                     [ | ||||
|                       GeneratedFormSwitch('rmAppEntry', | ||||
|                           label: tr('removeFromObtainium'), defaultValue: true) | ||||
|                     ], | ||||
|                     [ | ||||
|                       GeneratedFormSwitch('uninstallApp', | ||||
|                           label: tr('uninstallFromDevice')) | ||||
|                     ] | ||||
|                   ], | ||||
|             initValid: true, | ||||
|           ); | ||||
|         }); | ||||
|     if (values != null) { | ||||
|       bool uninstall = values['uninstallApp'] == true && showUninstallOption; | ||||
|       bool remove = values['rmAppEntry'] == true || !showUninstallOption; | ||||
|       if (uninstall) { | ||||
|         for (var i = 0; i < apps.length; i++) { | ||||
|           if (apps[i].installedVersion != null) { | ||||
|             uninstallApp(apps[i].id); | ||||
|             apps[i].installedVersion = null; | ||||
|           } | ||||
|         } | ||||
|         await saveApps(apps, attemptToCorrectInstallStatus: false); | ||||
|       } | ||||
|       if (remove) { | ||||
|         await removeApps(apps.map((e) => e.id).toList()); | ||||
|       } | ||||
|       return uninstall || remove; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   Future<void> openAppSettings(String appId) async { | ||||
|     final AndroidIntent intent = AndroidIntent( | ||||
|       action: 'action_application_details_settings', | ||||
|       data: 'package:$appId', | ||||
|     ); | ||||
|     await intent.launch(); | ||||
|   } | ||||
|  | ||||
|   Future<App?> checkUpdate(String appId) async { | ||||
|     App? currentApp = apps[appId]!.app; | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
| @@ -704,7 +771,7 @@ class AppsProvider with ChangeNotifier { | ||||
|       exportDir = await getExternalStorageDirectory(); | ||||
|       path = exportDir!.path; | ||||
|     } | ||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) { | ||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         await Permission.storage.request(); | ||||
|       } | ||||
|   | ||||
| @@ -225,7 +225,19 @@ class AppSource { | ||||
|         label: tr('trackOnly'), | ||||
|       ) | ||||
|     ], | ||||
|     [GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))] | ||||
|     [ | ||||
|       GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection')) | ||||
|     ], | ||||
|     [ | ||||
|       GeneratedFormTextField('apkFilterRegEx', | ||||
|           label: tr('filterAPKsByRegEx'), | ||||
|           required: false, | ||||
|           additionalValidators: [ | ||||
|             (value) { | ||||
|               return regExValidator(value); | ||||
|             } | ||||
|           ]) | ||||
|     ] | ||||
|   ]; | ||||
|  | ||||
|   // Previous 2 variables combined into one at runtime for convenient usage | ||||
| @@ -269,6 +281,18 @@ abstract class MassAppUrlSource { | ||||
|   Future<Map<String, String>> getUrlsWithDescriptions(List<String> args); | ||||
| } | ||||
|  | ||||
| regExValidator(String? value) { | ||||
|   if (value == null || value.isEmpty) { | ||||
|     return null; | ||||
|   } | ||||
|   try { | ||||
|     RegExp(value); | ||||
|   } catch (e) { | ||||
|     return tr('invalidRegEx'); | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| class SourceProvider { | ||||
|   // Add more source classes here so they are available via the service | ||||
|   List<AppSource> sources = [ | ||||
| @@ -344,10 +368,13 @@ class SourceProvider { | ||||
|   } | ||||
|  | ||||
|   Future<App> getApp( | ||||
|       AppSource source, String url, Map<String, dynamic> additionalSettings, | ||||
|       {App? currentApp, | ||||
|     AppSource source, | ||||
|     String url, | ||||
|     Map<String, dynamic> additionalSettings, { | ||||
|     App? currentApp, | ||||
|     bool trackOnlyOverride = false, | ||||
|       noVersionDetectionOverride = false}) async { | ||||
|     noVersionDetectionOverride = false, | ||||
|   }) async { | ||||
|     if (trackOnlyOverride || source.enforceTrackOnly) { | ||||
|       additionalSettings['trackOnly'] = true; | ||||
|     } | ||||
| @@ -358,6 +385,11 @@ class SourceProvider { | ||||
|     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); | ||||
|     APKDetails apk = | ||||
|         await source.getLatestAPKDetails(standardUrl, additionalSettings); | ||||
|     if (additionalSettings['apkFilterRegEx'] != null) { | ||||
|       var reg = RegExp(additionalSettings['apkFilterRegEx']); | ||||
|       apk.apkUrls = | ||||
|           apk.apkUrls.where((element) => reg.hasMatch(element)).toList(); | ||||
|     } | ||||
|     if (apk.apkUrls.isEmpty && !trackOnly) { | ||||
|       throw NoAPKError(); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										412
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						| @@ -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.10.3+109 # When changing this, update the tag in main() accordingly | ||||
| version: 0.10.7+113 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
| @@ -58,6 +58,7 @@ dependencies: | ||||
|   android_alarm_manager_plus: ^2.1.0 | ||||
|   sqflite: ^2.2.0+3 | ||||
|   easy_localization: ^3.0.1 | ||||
|   android_intent_plus: ^3.1.5 | ||||
|  | ||||
|  | ||||
| dev_dependencies: | ||||
|   | ||||