mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 21:43:29 +01:00 
			
		
		
		
	Compare commits
	
		
			26 Commits
		
	
	
		
			v0.9.0-bet
			...
			v0.9.5-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8e487592b3 | ||
|  | e9a44746a5 | ||
|  | 9123737bf3 | ||
|  | 12f70951c2 | ||
|  | c1d56f89f0 | ||
|  | 4dfd29f5de | ||
|  | 226cfa25e0 | ||
|  | 4e0c655538 | ||
|  | 45a23e9025 | ||
|  | 1e5aa0999a | ||
|  | beeec356e5 | ||
|  | 01fa9a2e96 | ||
|  | 0da7a36f1a | ||
|  | ed2a4e674f | ||
|  | 0f6a683faa | ||
|  | fa4d46b622 | ||
|  | a3f9947f28 | ||
|  | 6977858b99 | ||
|  | 2ff6acb701 | ||
|  | 0c2d6ce84d | ||
|  | 9072862862 | ||
|  | 3cbaac2f5d | ||
|  | 0f8871efcb | ||
|  | ee216cbbba | ||
|  | ebe5b79dc5 | ||
|  | 60014c864c | 
| @@ -15,7 +15,8 @@ Currently supported App sources: | ||||
| - [Signal](https://signal.org/) | ||||
| - [SourceForge](https://sourceforge.net/) | ||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||
| - Third Party F-Droid Repos (URLs ending with `/fdroid/repo`) | ||||
| - 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) | ||||
|  | ||||
| ## Limitations | ||||
|   | ||||
| @@ -135,7 +135,7 @@ | ||||
|     "appearance": "Aussehen", | ||||
|     "showWebInAppView": "Quellwebseite in der App-Ansicht anzeigen", | ||||
|     "pinUpdates": "Apps mit Aktualisierungen oben anheften", | ||||
|     "updates": "Aktualisiert", | ||||
|     "updates": "Aktualisierungen", | ||||
|     "sourceSpecific": "Quellenspezifisch", | ||||
|     "appSource": "App-Quelle", | ||||
|     "noLogs": "Keine Protokolle", | ||||
| @@ -188,25 +188,27 @@ | ||||
|     "steam": "Steam", | ||||
|     "steamMobile": "Steam Mobile", | ||||
|     "steamChat": "Steam Chat", | ||||
|     "install": "Install", | ||||
|     "markInstalled": "Mark Installed", | ||||
|     "update": "Update", | ||||
|     "markUpdated": "Mark Updated", | ||||
|     "additionalOptions": "Additional Options", | ||||
|     "disableVersionDetection": "Disable Version Detection", | ||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", | ||||
|     "downloadingX": "Downloading {}", | ||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", | ||||
|     "noAPKFound": "No APK found", | ||||
|     "noVersionDetection": "No version detection", | ||||
|     "categorize": "Categorize", | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "install": "Installieren", | ||||
|     "markInstalled": "Als Installiert markieren", | ||||
|     "update": "Aktualisieren", | ||||
|     "markUpdated": "Als Aktuell markieren", | ||||
|     "additionalOptions": "Zusätzliche Optionen", | ||||
|     "disableVersionDetection": "Versionsermittlung deaktivieren", | ||||
|     "noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.", | ||||
|     "downloadingX": "Lade {} herunter", | ||||
|     "downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App", | ||||
|     "noAPKFound": "Keine APK gefunden", | ||||
|     "noVersionDetection": "Keine Versionserkennung", | ||||
|     "categorize": "Kategorisieren", | ||||
|     "categories": "Kategorien", | ||||
|     "category": "Kategorie", | ||||
|     "noCategory": "Keine Kategorie", | ||||
|     "noCategories": "Keine Kategorien", | ||||
|     "deleteCategoriesQuestion": "Kategorien löschen?", | ||||
|     "categoryDeleteWarning": "Alle Apps in gelöschten Kategorien werden auf nicht kategorisiert gesetzt.", | ||||
|     "addCategory": "Kategorie hinzufügen", | ||||
|     "label": "Bezeichnung", | ||||
|     "language": "Sprache", | ||||
|     "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" | ||||
|   | ||||
| @@ -135,7 +135,7 @@ | ||||
|     "appearance": "Appearance", | ||||
|     "showWebInAppView": "Show Source Webpage in App View", | ||||
|     "pinUpdates": "Pin Updates to Top of Apps View", | ||||
|     "updates": "Updated", | ||||
|     "updates": "Updates", | ||||
|     "sourceSpecific": "Source-Specific", | ||||
|     "appSource": "App Source", | ||||
|     "noLogs": "No Logs", | ||||
| @@ -203,10 +203,12 @@ | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "noCategories": "No Categories", | ||||
|     "deleteCategoriesQuestion": "Delete Categories?", | ||||
|     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Too many requests (rate limited) - try again in {} minute", | ||||
|         "other": "Too many requests (rate limited) - try again in {} minutes" | ||||
|   | ||||
| @@ -13,10 +13,10 @@ | ||||
|     "and": "és", | ||||
|     "startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva", | ||||
|     "bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}", | ||||
|     "startedActualBGUpdateCheck": "Elkezdődött a tényleges BG frissítés ellenőrzése", | ||||
|     "startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése", | ||||
|     "bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött", | ||||
|     "firstRun": "Ez az Obtainium első futása", | ||||
|     "settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása a erre: {}", | ||||
|     "settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}", | ||||
|     "githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)", | ||||
|     "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", | ||||
|     "githubPATFormat": "felhasználónév:token", | ||||
| @@ -28,13 +28,13 @@ | ||||
|     "noDescription": "Nincs leírás", | ||||
|     "cancel": "Mégse", | ||||
|     "continue": "Tovább", | ||||
|     "requiredInBrackets": "(Kötlező)", | ||||
|     "dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓBAN KELL RENDELNI", | ||||
|     "requiredInBrackets": "(Kötelező)", | ||||
|     "dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI", | ||||
|     "colour": "Szín", | ||||
|     "githubStarredRepos": "GitHub Csillagozott Repo-k", | ||||
|     "githubStarredRepos": "GitHub Csillagos Repo-k", | ||||
|     "uname": "Felh.név", | ||||
|     "wrongArgNum": "Rossz számú argumentumot adott meg", | ||||
|     "xIsTrackOnly": "{} csak nyomon követhető", | ||||
|     "xIsTrackOnly": "A(z) {} csak nyomkövethető", | ||||
|     "source": "Forrás", | ||||
|     "app": "App", | ||||
|     "appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.", | ||||
| @@ -56,7 +56,7 @@ | ||||
|     "appsString": "Appok", | ||||
|     "noApps": "Nincs App", | ||||
|     "noAppsForFilter": "Nincsenek appok a szűrőhöz", | ||||
|     "byX": "By {}", | ||||
|     "byX": "{} által", | ||||
|     "percentProgress": "Folyamat: {}%", | ||||
|     "pleaseWait": "Kis türelmet", | ||||
|     "updateAvailable": "Frissítés elérhető", | ||||
| @@ -65,11 +65,11 @@ | ||||
|     "estimateInBrackets": "(Becslés)", | ||||
|     "selectAll": "Mindet kiválaszt", | ||||
|     "deselectN": "Törölje {} kijelölését", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", | ||||
|     "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", | ||||
|     "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?", | ||||
|     "removeSelectedApps": "Távolítsa el a kiválasztott appokat", | ||||
|     "updateX": "Frissítés: {}", | ||||
|     "installX": "Telepítés {}", | ||||
|     "installX": "Telepítés: {}", | ||||
|     "markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nas Frissítve", | ||||
|     "changeX": "Változás {}", | ||||
|     "installUpdateApps": "Appok telepítése/frissítése", | ||||
| @@ -118,7 +118,7 @@ | ||||
|     "selectURLs": "Kiválasztott URL-ek", | ||||
|     "pick": "Válasszon", | ||||
|     "theme": "Téma", | ||||
|     "dark": "Söét", | ||||
|     "dark": "Sötét", | ||||
|     "light": "Világos", | ||||
|     "followSystem": "Rendszer szerint", | ||||
|     "obtainium": "Obtainium", | ||||
| @@ -135,7 +135,7 @@ | ||||
|     "appearance": "Megjelenés", | ||||
|     "showWebInAppView": "Forrás megjelenítése az Appok nézetben", | ||||
|     "pinUpdates": "Frissítések kitűzése az App nézet tetejére", | ||||
|     "updates": "Frissítve", | ||||
|     "updates": "Frissítések", | ||||
|     "sourceSpecific": "Forrás-specifikus", | ||||
|     "appSource": "App forrás", | ||||
|     "noLogs": "Nincsenek naplók", | ||||
| @@ -145,7 +145,7 @@ | ||||
|     "appNotFound": "App nem található", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||
|     "pickAnAPK": "Válasszon egy APK-t", | ||||
|     "appHasMoreThanOnePackage": "{} egynél több csomaggal rendelkezik:", | ||||
|     "appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:", | ||||
|     "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", | ||||
|     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", | ||||
|     "warning": "Figyelem", | ||||
| @@ -153,7 +153,7 @@ | ||||
|     "updatesAvailable": "Frissítések érhetők el", | ||||
|     "updatesAvailableNotifDescription": "Értesíti a felhasználót, hogy frissítések állnak rendelkezésre egy vagy több, az Obtainium által nyomon követett alkalmazáshoz", | ||||
|     "noNewUpdates": "Nincsenek új frissítések.", | ||||
|     "xHasAnUpdate": "{} frissítést kapott.", | ||||
|     "xHasAnUpdate": "A(z) {} frissítést kapott.", | ||||
|     "appsUpdated": "Alkalmazások frissítve", | ||||
|     "appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több app frissítése történt a háttérben", | ||||
|     "xWasUpdatedToY": "{} frissítve a következőre: {}.", | ||||
| @@ -184,29 +184,29 @@ | ||||
|     "appIdOrName": "App ID vagy név", | ||||
|     "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel", | ||||
|     "reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Harmadik fél Repo", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo", | ||||
|     "steam": "Steam", | ||||
|     "steamMobile": "Steam Mobile", | ||||
|     "steamChat": "Steam Chat", | ||||
|     "install": "Install", | ||||
|     "markInstalled": "Mark Installed", | ||||
|     "update": "Update", | ||||
|     "markUpdated": "Mark Updated", | ||||
|     "additionalOptions": "Additional Options", | ||||
|     "disableVersionDetection": "Disable Version Detection", | ||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", | ||||
|     "downloadingX": "Downloading {}", | ||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", | ||||
|     "noAPKFound": "No APK found", | ||||
|     "noVersionDetection": "No version detection", | ||||
|     "categorize": "Categorize", | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "install": "Telepít", | ||||
|     "markInstalled": "Telepítettnek jelöl", | ||||
|     "update": "Frissít", | ||||
|     "markUpdated": "Frissítettnek jelöl", | ||||
|     "additionalOptions": "További lehetőségek", | ||||
|     "disableVersionDetection": "Verzióérzékelés letiltása", | ||||
|     "noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.", | ||||
|     "downloadingX": "{} letöltés", | ||||
|     "downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról", | ||||
|     "noAPKFound": "Nem található APK", | ||||
|     "noVersionDetection": "Nincs verzióérzékelés", | ||||
|     "categorize": "Kategorizálás", | ||||
|     "categories": "Kategóriák", | ||||
|     "category": "Kategória", | ||||
|     "noCategory": "Nincs kategória", | ||||
|     "deleteCategoryQuestion": "Törli a kategóriát?", | ||||
|     "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.", | ||||
|     "addCategory": "Új kategória", | ||||
|     "label": "Címke", | ||||
|     "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" | ||||
|   | ||||
| @@ -135,7 +135,7 @@ | ||||
|     "appearance": "Aspetto", | ||||
|     "showWebInAppView": "Mostra pagina web dell'App se selezionata", | ||||
|     "pinUpdates": "Fissa in alto gli aggiornamenti disponibili", | ||||
|     "updates": "Aggiornato", | ||||
|     "updates": "Aggiornamenti", | ||||
|     "sourceSpecific": "Specifiche per la fonte", | ||||
|     "appSource": "Sorgente dell'App", | ||||
|     "noLogs": "Nessun log", | ||||
| @@ -203,10 +203,12 @@ | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "noCategories": "No Categories", | ||||
|     "deleteCategoriesQuestion": "Delete Categories?", | ||||
|     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", | ||||
|         "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", | ||||
|     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", | ||||
|     "firstRun": "これがObtainiumの最初の実行です", | ||||
|     "settingUpdateCheckIntervalTo": "更新間隔を{}に設定する", | ||||
|     "settingUpdateCheckIntervalTo": "確認間隔を{}に設定する", | ||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", | ||||
|     "githubPATFormat": "ユーザー名:トークン", | ||||
| @@ -27,7 +27,7 @@ | ||||
|     "invalidRegEx": "無効な正規表現", | ||||
|     "noDescription": "説明はありません", | ||||
|     "cancel": "キャンセル", | ||||
|     "continue": "続ける", | ||||
|     "continue": "続行", | ||||
|     "requiredInBrackets": "(必須)", | ||||
|     "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です", | ||||
|     "colour": "カラー", | ||||
| @@ -64,7 +64,7 @@ | ||||
|     "notInstalled": "未インストール", | ||||
|     "estimateInBrackets": "(推定)", | ||||
|     "selectAll": "すべて選択", | ||||
|     "deselectN": "{}件を選択解除", | ||||
|     "deselectN": "{}件の選択を解除", | ||||
|     "xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。", | ||||
|     "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", | ||||
|     "removeSelectedApps": "選択したアプリを削除する", | ||||
| @@ -124,18 +124,18 @@ | ||||
|     "obtainium": "Obtainium", | ||||
|     "materialYou": "Material You", | ||||
|     "appSortBy": "アプリの並び方", | ||||
|     "authorName": "作者/アプリ名", | ||||
|     "nameAuthor": "アプリ名/作者", | ||||
|     "authorName": "作者名/アプリ名", | ||||
|     "nameAuthor": "アプリ名/作者名", | ||||
|     "asAdded": "追加順", | ||||
|     "appSortOrder": "並び順", | ||||
|     "ascending": "昇順", | ||||
|     "descending": "降順", | ||||
|     "bgUpdateCheckInterval": "バックグラウンド更新の確認間隔", | ||||
|     "bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔", | ||||
|     "neverManualOnly": "手動", | ||||
|     "appearance": "外観", | ||||
|     "showWebInAppView": "アプリビューにソースウェブページを表示する", | ||||
|     "pinUpdates": "アップデートがあるアプリをトップに固定する", | ||||
|     "updates": "更新", | ||||
|     "updates": "アップデート", | ||||
|     "sourceSpecific": "Github アクセストークン", | ||||
|     "appSource": "アプリのソース", | ||||
|     "noLogs": "ログはありません", | ||||
| @@ -144,7 +144,7 @@ | ||||
|     "share": "共有", | ||||
|     "appNotFound": "アプリが見つかりません", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-エクスポート", | ||||
|     "pickAnAPK": "APKを選ぶ", | ||||
|     "pickAnAPK": "APKを選択", | ||||
|     "appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ", | ||||
|     "deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。", | ||||
|     "deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:", | ||||
| @@ -199,25 +199,27 @@ | ||||
|     "downloadNotifDescription": "アプリのダウンロード状況を通知する", | ||||
|     "noAPKFound": "APKが見つかりません", | ||||
|     "noVersionDetection": "バージョン検出を行わない", | ||||
|     "categorize": "Categorize", | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "categorize": "カテゴライズ", | ||||
|     "categories": "カテゴリ", | ||||
|     "category": "カテゴリ", | ||||
|     "noCategory": "カテゴリなし", | ||||
|     "noCategories": "カテゴリなし", | ||||
|     "deleteCategoriesQuestion": "カテゴリを削除しますか?", | ||||
|     "categoryDeleteWarning": "削除されたカテゴリ内のアプリは未分類に設定されます。", | ||||
|     "addCategory": "カテゴリを追加", | ||||
|     "label": "ラベル", | ||||
|     "language": "言語", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||
|     }, | ||||
|     "bgUpdateGotErrorRetryInMinutes": { | ||||
|         "one": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します", | ||||
|         "other": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します" | ||||
|         "one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します", | ||||
|         "other": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します" | ||||
|     }, | ||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||
|         "one": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します", | ||||
|         "other": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" | ||||
|         "one": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します", | ||||
|         "other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" | ||||
|     }, | ||||
|     "apps": { | ||||
|         "one": "{}個のアプリ", | ||||
|   | ||||
| @@ -203,10 +203,12 @@ | ||||
|     "categories": "Categories", | ||||
|     "category": "Category", | ||||
|     "noCategory": "No Category", | ||||
|     "deleteCategoryQuestion": "Delete Category?", | ||||
|     "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", | ||||
|     "noCategories": "No Categories", | ||||
|     "deleteCategoriesQuestion": "Delete Categories?", | ||||
|     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||
|     "addCategory": "Add Category", | ||||
|     "label": "Label", | ||||
|     "language": "Language", | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", | ||||
|         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class APKMirror extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await get(Uri.parse('$standardUrl/feed')); | ||||
|     if (res.statusCode == 200) { | ||||
|   | ||||
| @@ -32,7 +32,7 @@ class FDroid extends AppSource { | ||||
|  | ||||
|   @override | ||||
|   String? tryInferringAppId(String standardUrl, | ||||
|       {Map<String, String> additionalSettings = const {}}) { | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) { | ||||
|     return Uri.parse(standardUrl).pathSegments.last; | ||||
|   } | ||||
|  | ||||
| @@ -61,7 +61,7 @@ class FDroid extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     String? appId = tryInferringAppId(standardUrl); | ||||
|     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class FDroidRepo extends AppSource { | ||||
|  | ||||
|     additionalSourceAppSpecificSettingFormItems = [ | ||||
|       [ | ||||
|         GeneratedFormItem('appIdOrName', | ||||
|         GeneratedFormTextField('appIdOrName', | ||||
|             label: tr('appIdOrName'), | ||||
|             hint: tr('reposHaveMultipleApps'), | ||||
|             required: true) | ||||
| @@ -22,7 +22,7 @@ class FDroidRepo extends AppSource { | ||||
|   @override | ||||
|   String standardizeURL(String url) { | ||||
|     RegExp standardUrlRegExp = | ||||
|         RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)'); | ||||
|         RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)'); | ||||
|     RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
| @@ -33,7 +33,7 @@ class FDroidRepo extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     String? appIdOrName = additionalSettings['appIdOrName']; | ||||
|     if (appIdOrName == null) { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class GitHub extends AppSource { | ||||
|     host = 'github.com'; | ||||
|  | ||||
|     additionalSourceSpecificSettingFormItems = [ | ||||
|       GeneratedFormItem('github-creds', | ||||
|       GeneratedFormTextField('github-creds', | ||||
|           label: tr('githubPATLabel'), | ||||
|           required: false, | ||||
|           additionalValidators: [ | ||||
| @@ -51,21 +51,16 @@ class GitHub extends AppSource { | ||||
|  | ||||
|     additionalSourceAppSpecificSettingFormItems = [ | ||||
|       [ | ||||
|         GeneratedFormItem('includePrereleases', | ||||
|             label: tr('includePrereleases'), | ||||
|             type: FormItemType.bool, | ||||
|             defaultValue: '') | ||||
|         GeneratedFormSwitch('includePrereleases', | ||||
|             label: tr('includePrereleases'), defaultValue: false) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormItem('fallbackToOlderReleases', | ||||
|             label: tr('fallbackToOlderReleases'), | ||||
|             type: FormItemType.bool, | ||||
|             defaultValue: 'true') | ||||
|         GeneratedFormSwitch('fallbackToOlderReleases', | ||||
|             label: tr('fallbackToOlderReleases'), defaultValue: true) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormItem('filterReleaseTitlesByRegEx', | ||||
|         GeneratedFormTextField('filterReleaseTitlesByRegEx', | ||||
|             label: tr('filterReleaseTitlesByRegEx'), | ||||
|             type: FormItemType.string, | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
| @@ -111,13 +106,15 @@ class GitHub extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var includePrereleases = additionalSettings['includePrereleases'] == 'true'; | ||||
|     var fallbackToOlderReleases = | ||||
|         additionalSettings['fallbackToOlderReleases'] == 'true'; | ||||
|     var regexFilter = | ||||
|         additionalSettings['filterReleaseTitlesByRegEx']?.isNotEmpty == true | ||||
|     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( | ||||
| @@ -150,7 +147,7 @@ class GitHub extends AppSource { | ||||
|           continue; | ||||
|         } | ||||
|         var apkUrls = getReleaseAPKUrls(releases[i]); | ||||
|         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != 'true') { | ||||
|         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { | ||||
|           continue; | ||||
|         } | ||||
|         targetRelease = releases[i]; | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class GitLab extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); | ||||
|     if (res.statusCode == 200) { | ||||
|   | ||||
| @@ -23,14 +23,14 @@ class IzzyOnDroid extends AppSource { | ||||
|  | ||||
|   @override | ||||
|   String? tryInferringAppId(String standardUrl, | ||||
|       {Map<String, String> additionalSettings = const {}}) { | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) { | ||||
|     return FDroid().tryInferringAppId(standardUrl); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     String? appId = tryInferringAppId(standardUrl); | ||||
|     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class Mullvad extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await get(Uri.parse('$standardUrl/en/download/android')); | ||||
|     if (res.statusCode == 200) { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ class Signal extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = | ||||
|         await get(Uri.parse('https://updates.$host/android/latest.json')); | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class SourceForge extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await get(Uri.parse('$standardUrl/rss?path=/')); | ||||
|     if (res.statusCode == 200) { | ||||
|   | ||||
| @@ -10,10 +10,7 @@ class SteamMobile extends AppSource { | ||||
|     host = 'store.steampowered.com'; | ||||
|     name = tr('steam'); | ||||
|     additionalSourceAppSpecificSettingFormItems = [ | ||||
|       [ | ||||
|         GeneratedFormItem('app', | ||||
|             label: tr('app'), required: true, opts: apks.entries.toList()) | ||||
|       ] | ||||
|       [GeneratedFormDropdown('app', apks.entries.toList(), label: tr('app'))] | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
| @@ -30,11 +27,11 @@ class SteamMobile extends AppSource { | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, String> additionalSettings, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await get(Uri.parse('https://$host/mobile')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var apkNamePrefix = additionalSettings['app']; | ||||
|       var apkNamePrefix = additionalSettings['app'] as String?; | ||||
|       if (apkNamePrefix == null) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|   | ||||
| @@ -1,38 +1,120 @@ | ||||
| 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'; | ||||
|  | ||||
| enum FormItemType { string, bool } | ||||
|  | ||||
| typedef OnValueChanges = void Function( | ||||
|     Map<String, String> values, bool valid, bool isBuilding); | ||||
|  | ||||
| class GeneratedFormItem { | ||||
| abstract class GeneratedFormItem { | ||||
|   late String key; | ||||
|   late String label; | ||||
|   late FormItemType type; | ||||
|   late bool required; | ||||
|   late int max; | ||||
|   late List<String? Function(String? value)> additionalValidators; | ||||
|   late List<Widget> belowWidgets; | ||||
|   late String? hint; | ||||
|   late List<MapEntry<String, String>>? opts; | ||||
|   late String? defaultValue; | ||||
|   late dynamic defaultValue; | ||||
|   List<dynamic> additionalValidators; | ||||
|   dynamic ensureType(dynamic val); | ||||
|  | ||||
|   GeneratedFormItem(this.key, | ||||
|       {this.label = 'Input', | ||||
|       this.type = FormItemType.string, | ||||
|       this.belowWidgets = const [], | ||||
|       this.defaultValue, | ||||
|       this.additionalValidators = const []}); | ||||
| } | ||||
|  | ||||
| class GeneratedFormTextField extends GeneratedFormItem { | ||||
|   late bool required; | ||||
|   late int max; | ||||
|   late String? hint; | ||||
|  | ||||
|   GeneratedFormTextField(String key, | ||||
|       {String label = 'Input', | ||||
|       List<Widget> belowWidgets = const [], | ||||
|       String defaultValue = '', | ||||
|       List<String? Function(String? value)> additionalValidators = const [], | ||||
|       this.required = true, | ||||
|       this.max = 1, | ||||
|       this.additionalValidators = const [], | ||||
|       this.belowWidgets = const [], | ||||
|       this.hint, | ||||
|       this.opts, | ||||
|       this.defaultValue}) { | ||||
|     if (type != FormItemType.string) { | ||||
|       required = false; | ||||
|       this.hint}) | ||||
|       : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
|             defaultValue: defaultValue, | ||||
|             additionalValidators: additionalValidators); | ||||
|  | ||||
|   @override | ||||
|   String ensureType(val) { | ||||
|     return val.toString(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class GeneratedFormDropdown extends GeneratedFormItem { | ||||
|   late List<MapEntry<String, String>>? opts; | ||||
|  | ||||
|   GeneratedFormDropdown( | ||||
|     String key, | ||||
|     this.opts, { | ||||
|     String label = 'Input', | ||||
|     List<Widget> belowWidgets = const [], | ||||
|     String defaultValue = '', | ||||
|     List<String? Function(String? value)> additionalValidators = const [], | ||||
|   }) : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
|             defaultValue: defaultValue, | ||||
|             additionalValidators: additionalValidators); | ||||
|  | ||||
|   @override | ||||
|   String ensureType(val) { | ||||
|     return val.toString(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class GeneratedFormSwitch extends GeneratedFormItem { | ||||
|   GeneratedFormSwitch( | ||||
|     String key, { | ||||
|     String label = 'Input', | ||||
|     List<Widget> belowWidgets = const [], | ||||
|     bool defaultValue = false, | ||||
|     List<String? Function(bool value)> additionalValidators = const [], | ||||
|   }) : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
|             defaultValue: defaultValue, | ||||
|             additionalValidators: additionalValidators); | ||||
|  | ||||
|   @override | ||||
|   bool ensureType(val) { | ||||
|     return val == true || val == 'true'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class GeneratedFormTagInput extends GeneratedFormItem { | ||||
|   late MapEntry<String, String>? deleteConfirmationMessage; | ||||
|   late bool singleSelect; | ||||
|   late WrapAlignment alignment; | ||||
|   late String emptyMessage; | ||||
|   GeneratedFormTagInput(String key, | ||||
|       {String label = 'Input', | ||||
|       List<Widget> belowWidgets = const [], | ||||
|       Map<String, MapEntry<int, bool>> defaultValue = const {}, | ||||
|       List<String? Function(Map<String, MapEntry<int, bool>> value)> | ||||
|           additionalValidators = const [], | ||||
|       this.deleteConfirmationMessage, | ||||
|       this.singleSelect = false, | ||||
|       this.alignment = WrapAlignment.start, | ||||
|       this.emptyMessage = 'Input'}) | ||||
|       : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
|             defaultValue: defaultValue, | ||||
|             additionalValidators: additionalValidators); | ||||
|  | ||||
|   @override | ||||
|   Map<String, MapEntry<int, bool>> ensureType(val) { | ||||
|     return val is Map<String, MapEntry<int, bool>> ? val : {}; | ||||
|   } | ||||
| } | ||||
|  | ||||
| typedef OnValueChanges = void Function( | ||||
|     Map<String, dynamic> values, bool valid, bool isBuilding); | ||||
|  | ||||
| class GeneratedForm extends StatefulWidget { | ||||
|   const GeneratedForm( | ||||
| @@ -47,18 +129,16 @@ class GeneratedForm extends StatefulWidget { | ||||
|  | ||||
| class _GeneratedFormState extends State<GeneratedForm> { | ||||
|   final _formKey = GlobalKey<FormState>(); | ||||
|   Map<String, String> values = {}; | ||||
|   Map<String, dynamic> values = {}; | ||||
|   late List<List<Widget>> formInputs; | ||||
|   List<List<Widget>> rows = []; | ||||
|  | ||||
|   // If any value changes, call this to update the parent with value and validity | ||||
|   void someValueChanged({bool isBuilding = false}) { | ||||
|     Map<String, String> returnValues = {}; | ||||
|     Map<String, dynamic> returnValues = values; | ||||
|     var valid = true; | ||||
|     for (int r = 0; r < widget.items.length; r++) { | ||||
|       for (int i = 0; i < widget.items[r].length; i++) { | ||||
|         returnValues[widget.items[r][i].key] = | ||||
|             values[widget.items[r][i].key] ?? ''; | ||||
|         if (formInputs[r][i] is TextFormField) { | ||||
|           valid = valid && | ||||
|               ((formInputs[r][i].key as GlobalKey<FormFieldState>) | ||||
| @@ -71,6 +151,21 @@ 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(); | ||||
| @@ -80,35 +175,37 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|     int j = 0; | ||||
|     for (var row in widget.items) { | ||||
|       for (var e in row) { | ||||
|         values[e.key] = e.defaultValue ?? e.opts?.first.key ?? ''; | ||||
|         values[e.key] = e.defaultValue; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Dynamically create form inputs | ||||
|     formInputs = widget.items.asMap().entries.map((row) { | ||||
|       return row.value.asMap().entries.map((e) { | ||||
|         if (e.value.type == FormItemType.string && e.value.opts == null) { | ||||
|         var formItem = e.value; | ||||
|         if (formItem is GeneratedFormTextField) { | ||||
|           final formFieldKey = GlobalKey<FormFieldState>(); | ||||
|           return TextFormField( | ||||
|             key: formFieldKey, | ||||
|             initialValue: values[e.value.key], | ||||
|             initialValue: values[formItem.key], | ||||
|             autovalidateMode: AutovalidateMode.onUserInteraction, | ||||
|             onChanged: (value) { | ||||
|               setState(() { | ||||
|                 values[e.value.key] = value; | ||||
|                 values[formItem.key] = value; | ||||
|                 someValueChanged(); | ||||
|               }); | ||||
|             }, | ||||
|             decoration: InputDecoration( | ||||
|                 helperText: e.value.label + (e.value.required ? ' *' : ''), | ||||
|                 hintText: e.value.hint), | ||||
|             minLines: e.value.max <= 1 ? null : e.value.max, | ||||
|             maxLines: e.value.max <= 1 ? 1 : e.value.max, | ||||
|                 helperText: formItem.label + (formItem.required ? ' *' : ''), | ||||
|                 hintText: formItem.hint), | ||||
|             minLines: formItem.max <= 1 ? null : formItem.max, | ||||
|             maxLines: formItem.max <= 1 ? 1 : formItem.max, | ||||
|             validator: (value) { | ||||
|               if (e.value.required && (value == null || value.trim().isEmpty)) { | ||||
|                 return '${e.value.label} ${tr('requiredInBrackets')}'; | ||||
|               if (formItem.required && | ||||
|                   (value == null || value.trim().isEmpty)) { | ||||
|                 return '${formItem.label} ${tr('requiredInBrackets')}'; | ||||
|               } | ||||
|               for (var validator in e.value.additionalValidators) { | ||||
|               for (var validator in formItem.additionalValidators) { | ||||
|                 String? result = validator(value); | ||||
|                 if (result != null) { | ||||
|                   return result; | ||||
| @@ -117,21 +214,20 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|               return null; | ||||
|             }, | ||||
|           ); | ||||
|         } else if (e.value.type == FormItemType.string && | ||||
|             e.value.opts != null) { | ||||
|           if (e.value.opts!.isEmpty) { | ||||
|         } else if (formItem is GeneratedFormDropdown) { | ||||
|           if (formItem.opts!.isEmpty) { | ||||
|             return Text(tr('dropdownNoOptsError')); | ||||
|           } | ||||
|           return DropdownButtonFormField( | ||||
|               decoration: InputDecoration(labelText: e.value.label), | ||||
|               value: values[e.value.key], | ||||
|               items: e.value.opts! | ||||
|                   .map((e) => | ||||
|                       DropdownMenuItem(value: e.key, child: Text(e.value))) | ||||
|               decoration: InputDecoration(labelText: formItem.label), | ||||
|               value: values[formItem.key], | ||||
|               items: formItem.opts! | ||||
|                   .map((e2) => | ||||
|                       DropdownMenuItem(value: e2.key, child: Text(e2.value))) | ||||
|                   .toList(), | ||||
|               onChanged: (value) { | ||||
|                 setState(() { | ||||
|                   values[e.value.key] = value ?? e.value.opts!.first.key; | ||||
|                   values[formItem.key] = value ?? formItem.opts!.first.key; | ||||
|                   someValueChanged(); | ||||
|                 }); | ||||
|               }); | ||||
| @@ -147,21 +243,173 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|   Widget build(BuildContext context) { | ||||
|     for (var r = 0; r < formInputs.length; r++) { | ||||
|       for (var e = 0; e < formInputs[r].length; e++) { | ||||
|         if (widget.items[r][e].type == FormItemType.bool) { | ||||
|         if (widget.items[r][e] is GeneratedFormSwitch) { | ||||
|           formInputs[r][e] = Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|             children: [ | ||||
|               Text(widget.items[r][e].label), | ||||
|               Switch( | ||||
|                   value: values[widget.items[r][e].key] == 'true', | ||||
|                   value: values[widget.items[r][e].key], | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       values[widget.items[r][e].key] = value ? 'true' : ''; | ||||
|                       values[widget.items[r][e].key] = value; | ||||
|                       someValueChanged(); | ||||
|                     }); | ||||
|                   }) | ||||
|             ], | ||||
|           ); | ||||
|         } else if (widget.items[r][e] is GeneratedFormTagInput) { | ||||
|           formInputs[r][e] = Wrap( | ||||
|             alignment: (widget.items[r][e] as GeneratedFormTagInput).alignment, | ||||
|             crossAxisAlignment: WrapCrossAlignment.center, | ||||
|             children: [ | ||||
|               (values[widget.items[r][e].key] | ||||
|                               as Map<String, MapEntry<int, bool>>?) | ||||
|                           ?.isEmpty == | ||||
|                       true | ||||
|                   ? Text( | ||||
|                       (widget.items[r][e] as GeneratedFormTagInput) | ||||
|                           .emptyMessage, | ||||
|                       style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ) | ||||
|                   : const SizedBox.shrink(), | ||||
|               ...(values[widget.items[r][e].key] | ||||
|                           as Map<String, MapEntry<int, bool>>?) | ||||
|                       ?.entries | ||||
|                       .map((e2) { | ||||
|                     return Padding( | ||||
|                         padding: const EdgeInsets.symmetric(horizontal: 4), | ||||
|                         child: ChoiceChip( | ||||
|                           label: Text(e2.key), | ||||
|                           backgroundColor: Color(e2.value.key).withAlpha(50), | ||||
|                           selectedColor: Color(e2.value.key), | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                           selected: e2.value.value, | ||||
|                           onSelected: (value) { | ||||
|                             setState(() { | ||||
|                               (values[widget.items[r][e].key] as Map<String, | ||||
|                                       MapEntry<int, bool>>)[e2.key] = | ||||
|                                   MapEntry( | ||||
|                                       (values[widget.items[r][e].key] as Map< | ||||
|                                               String, | ||||
|                                               MapEntry<int, bool>>)[e2.key]! | ||||
|                                           .key, | ||||
|                                       value); | ||||
|                               if ((widget.items[r][e] as GeneratedFormTagInput) | ||||
|                                       .singleSelect && | ||||
|                                   value == true) { | ||||
|                                 for (var key in (values[widget.items[r][e].key] | ||||
|                                         as Map<String, MapEntry<int, bool>>) | ||||
|                                     .keys) { | ||||
|                                   if (key != e2.key) { | ||||
|                                     (values[widget.items[r][e].key] as Map< | ||||
|                                         String, | ||||
|                                         MapEntry<int, | ||||
|                                             bool>>)[key] = MapEntry( | ||||
|                                         (values[widget.items[r][e].key] as Map< | ||||
|                                                 String, | ||||
|                                                 MapEntry<int, bool>>)[key]! | ||||
|                                             .key, | ||||
|                                         false); | ||||
|                                   } | ||||
|                                 } | ||||
|                               } | ||||
|                               someValueChanged(); | ||||
|                             }); | ||||
|                           }, | ||||
|                         )); | ||||
|                   }) ?? | ||||
|                   [const SizedBox.shrink()], | ||||
|               (values[widget.items[r][e].key] | ||||
|                               as Map<String, MapEntry<int, bool>>?) | ||||
|                           ?.values | ||||
|                           .where((e) => e.value) | ||||
|                           .isNotEmpty == | ||||
|                       true | ||||
|                   ? Padding( | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 4), | ||||
|                       child: IconButton( | ||||
|                         onPressed: () { | ||||
|                           fn() { | ||||
|                             setState(() { | ||||
|                               var temp = values[widget.items[r][e].key] | ||||
|                                   as Map<String, MapEntry<int, bool>>; | ||||
|                               temp.removeWhere((key, value) => value.value); | ||||
|                               values[widget.items[r][e].key] = temp; | ||||
|                               someValueChanged(); | ||||
|                             }); | ||||
|                           } | ||||
|  | ||||
|                           if ((widget.items[r][e] as GeneratedFormTagInput) | ||||
|                                   .deleteConfirmationMessage != | ||||
|                               null) { | ||||
|                             var message = | ||||
|                                 (widget.items[r][e] as GeneratedFormTagInput) | ||||
|                                     .deleteConfirmationMessage!; | ||||
|                             showDialog<Map<String, dynamic>?>( | ||||
|                                 context: context, | ||||
|                                 builder: (BuildContext ctx) { | ||||
|                                   return GeneratedFormModal( | ||||
|                                       title: message.key, | ||||
|                                       message: message.value, | ||||
|                                       items: const []); | ||||
|                                 }).then((value) { | ||||
|                               if (value != null) { | ||||
|                                 fn(); | ||||
|                               } | ||||
|                             }); | ||||
|                           } else { | ||||
|                             fn(); | ||||
|                           } | ||||
|                         }, | ||||
|                         icon: const Icon(Icons.remove), | ||||
|                         visualDensity: VisualDensity.compact, | ||||
|                         tooltip: tr('remove'), | ||||
|                       )) | ||||
|                   : const SizedBox.shrink(), | ||||
|               Padding( | ||||
|                   padding: const EdgeInsets.symmetric(horizontal: 4), | ||||
|                   child: IconButton( | ||||
|                     onPressed: () { | ||||
|                       showDialog<Map<String, dynamic>?>( | ||||
|                           context: context, | ||||
|                           builder: (BuildContext ctx) { | ||||
|                             return GeneratedFormModal( | ||||
|                                 title: widget.items[r][e].label, | ||||
|                                 items: [ | ||||
|                                   [ | ||||
|                                     GeneratedFormTextField('label', | ||||
|                                         label: tr('label')) | ||||
|                                   ] | ||||
|                                 ]); | ||||
|                           }).then((value) { | ||||
|                         String? label = value?['label']; | ||||
|                         if (label != null) { | ||||
|                           setState(() { | ||||
|                             var temp = values[widget.items[r][e].key] | ||||
|                                 as Map<String, MapEntry<int, bool>>?; | ||||
|                             temp ??= {}; | ||||
|                             var singleSelect = | ||||
|                                 (widget.items[r][e] as GeneratedFormTagInput) | ||||
|                                     .singleSelect; | ||||
|                             var someSelected = temp.entries | ||||
|                                 .where((element) => element.value.value) | ||||
|                                 .isNotEmpty; | ||||
|                             temp[label] = MapEntry( | ||||
|                                 generateRandomLightColor().value, | ||||
|                                 !(someSelected && singleSelect)); | ||||
|                             values[widget.items[r][e].key] = temp; | ||||
|                             someValueChanged(); | ||||
|                           }); | ||||
|                         } | ||||
|                       }); | ||||
|                     }, | ||||
|                     icon: const Icon(Icons.add), | ||||
|                     visualDensity: VisualDensity.compact, | ||||
|                     tooltip: tr('add'), | ||||
|                   )), | ||||
|             ], | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -171,9 +419,8 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|       if (rowInputs.key > 0) { | ||||
|         rows.add([ | ||||
|           SizedBox( | ||||
|             height: widget.items[rowInputs.key][0].type == FormItemType.bool && | ||||
|                     widget.items[rowInputs.key - 1][0].type == | ||||
|                         FormItemType.string | ||||
|             height: widget.items[rowInputs.key][0] is GeneratedFormSwitch && | ||||
|                     widget.items[rowInputs.key - 1][0] is! GeneratedFormSwitch | ||||
|                 ? 25 | ||||
|                 : 8, | ||||
|           ) | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class GeneratedFormModal extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _GeneratedFormModalState extends State<GeneratedFormModal> { | ||||
|   Map<String, String> values = {}; | ||||
|   Map<String, dynamic> values = {}; | ||||
|   bool valid = false; | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -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.0'; | ||||
| const String currentVersion = '0.9.5'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
| @@ -43,12 +43,16 @@ final globalNavigatorKey = GlobalKey<NavigatorState>(); | ||||
| Future<void> loadTranslations() async { | ||||
|   // See easy_localization/issues/210 | ||||
|   await EasyLocalizationController.initEasyLocation(); | ||||
|   var s = SettingsProvider(); | ||||
|   await s.initializeSettings(); | ||||
|   var forceLocale = s.forcedLocale; | ||||
|   final controller = EasyLocalizationController( | ||||
|     saveLocale: true, | ||||
|     forceLocale: forceLocale != null ? Locale(forceLocale) : null, | ||||
|     fallbackLocale: fallbackLocale, | ||||
|     supportedLocales: supportedLocales, | ||||
|     assetLoader: const RootBundleAssetLoader(), | ||||
|     useOnlyLangCode: false, | ||||
|     useOnlyLangCode: true, | ||||
|     useFallbackTranslations: true, | ||||
|     path: localeDir, | ||||
|     onLoadError: (FlutterError e) { | ||||
| @@ -160,6 +164,7 @@ void main() async { | ||||
|         supportedLocales: supportedLocales, | ||||
|         path: localeDir, | ||||
|         fallbackLocale: fallbackLocale, | ||||
|         useOnlyLangCode: true, | ||||
|         child: const Obtainium()), | ||||
|   )); | ||||
| } | ||||
| @@ -200,7 +205,7 @@ class _ObtainiumState extends State<Obtainium> { | ||||
|               currentReleaseTag, | ||||
|               [], | ||||
|               0, | ||||
|               {'includePrereleases': 'true'}, | ||||
|               {'includePrereleases': true}, | ||||
|               null, | ||||
|               false) | ||||
|         ]); | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|   String userInput = ''; | ||||
|   String searchQuery = ''; | ||||
|   AppSource? pickedSource; | ||||
|   Map<String, String> additionalSettings = {}; | ||||
|   Map<String, dynamic> additionalSettings = {}; | ||||
|   bool additionalSettingsValid = true; | ||||
|  | ||||
|   @override | ||||
| @@ -66,9 +66,9 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|       }); | ||||
|       var settingsProvider = context.read<SettingsProvider>(); | ||||
|       () async { | ||||
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == 'true'; | ||||
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == true; | ||||
|         var userPickedNoVersionDetection = | ||||
|             additionalSettings['noVersionDetection'] == 'true'; | ||||
|             additionalSettings['noVersionDetection'] == true; | ||||
|         var cont = true; | ||||
|         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && | ||||
|             await showDialog( | ||||
| @@ -113,7 +113,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           } | ||||
|           // Only download the APK here if you need to for the package ID | ||||
|           if (sourceProvider.isTempId(app.id) && | ||||
|               app.additionalSettings['trackOnly'] != 'true') { | ||||
|               app.additionalSettings['trackOnly'] != true) { | ||||
|             // ignore: use_build_context_synchronously | ||||
|             var apkUrl = await appsProvider.confirmApkUrl(app, context); | ||||
|             if (apkUrl == null) { | ||||
| @@ -128,7 +128,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|           if (appsProvider.apps.containsKey(app.id)) { | ||||
|             throw ObtainiumError(tr('appAlreadyAdded')); | ||||
|           } | ||||
|           if (app.additionalSettings['trackOnly'] == 'true') { | ||||
|           if (app.additionalSettings['trackOnly'] == true) { | ||||
|             app.installedVersion = app.latestVersion; | ||||
|           } | ||||
|           await appsProvider.saveApps([app]); | ||||
| @@ -169,7 +169,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                               child: GeneratedForm( | ||||
|                                   items: [ | ||||
|                                 [ | ||||
|                                   GeneratedFormItem('appSourceURL', | ||||
|                                   GeneratedFormTextField('appSourceURL', | ||||
|                                       label: tr('appSourceURL'), | ||||
|                                       additionalValidators: [ | ||||
|                                         (value) { | ||||
| @@ -231,7 +231,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                               child: GeneratedForm( | ||||
|                                   items: [ | ||||
|                                     [ | ||||
|                                       GeneratedFormItem('searchSomeSources', | ||||
|                                       GeneratedFormTextField( | ||||
|                                           'searchSomeSources', | ||||
|                                           label: tr('searchSomeSourcesLabel'), | ||||
|                                           required: false), | ||||
|                                     ] | ||||
|   | ||||
| @@ -5,6 +5,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/main.dart'; | ||||
| import 'package:obtainium/pages/settings.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| @@ -42,7 +43,7 @@ class _AppPageState extends State<AppPage> { | ||||
|       prevApp = app; | ||||
|       getUpdate(app.app.id); | ||||
|     } | ||||
|     var trackOnly = app?.app.additionalSettings['trackOnly'] == 'true'; | ||||
|     var trackOnly = app?.app.additionalSettings['trackOnly'] == true; | ||||
|     return Scaffold( | ||||
|       appBar: settingsProvider.showAppWebpage ? AppBar() : null, | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
| @@ -152,49 +153,22 @@ class _AppPageState extends State<AppPage> { | ||||
|                               fontStyle: FontStyle.italic, fontSize: 12), | ||||
|                         ), | ||||
|                         const SizedBox( | ||||
|                           height: 32, | ||||
|                           height: 48, | ||||
|                         ), | ||||
|                         app?.app.category != null | ||||
|                             ? Chip( | ||||
|                                 label: Text(app!.app.category!), | ||||
|                                 backgroundColor: | ||||
|                                     Color(categories[app.app.category!] ?? 0x0), | ||||
|                                 onDeleted: () { | ||||
|                                   app.app.category = null; | ||||
|                                   appsProvider.saveApps([app.app]); | ||||
|                                 }, | ||||
|                                 visualDensity: VisualDensity.compact, | ||||
|                               ) | ||||
|                             : Row( | ||||
|                                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                     TextButton( | ||||
|                                         onPressed: () { | ||||
|                                           showDialog<Map<String, String>?>( | ||||
|                                               context: context, | ||||
|                                               builder: (BuildContext ctx) { | ||||
|                                                 return GeneratedFormModal( | ||||
|                                                     title: 'Pick a Category', | ||||
|                                                     items: [ | ||||
|                                                       [ | ||||
|                                                         settingsProvider | ||||
|                                                             .getCategoryFormItem() | ||||
|                                                       ] | ||||
|                                                     ]); | ||||
|                                               }).then((value) { | ||||
|                                             if (value != null && app != null) { | ||||
|                                               String? cat = (value['category'] | ||||
|                                                           ?.isNotEmpty ?? | ||||
|                                                       false) | ||||
|                                                   ? value['category'] | ||||
|                         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; | ||||
|                                               app.app.category = cat; | ||||
|                                 appsProvider.saveApps([app.app]); | ||||
|                               } | ||||
|                                           }); | ||||
|                                         }, | ||||
|                                         child: Text(tr('categorize'))) | ||||
|                                   ]) | ||||
|                             }) | ||||
|                       ], | ||||
|                     )), | ||||
|                   ], | ||||
| @@ -273,7 +247,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                               onPressed: app?.downloadProgress != null | ||||
|                                   ? null | ||||
|                                   : () { | ||||
|                                       showDialog<Map<String, String>>( | ||||
|                                       showDialog<Map<String, dynamic>?>( | ||||
|                                           context: context, | ||||
|                                           builder: (BuildContext ctx) { | ||||
|                                             var items = source | ||||
| @@ -301,7 +275,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                                               values; | ||||
|                                           if (source.enforceTrackOnly) { | ||||
|                                             changedApp.additionalSettings[ | ||||
|                                                 'trackOnly'] = 'true'; | ||||
|                                                 'trackOnly'] = true; | ||||
|                                             showError( | ||||
|                                                 tr('appsFromSourceAreTrackOnly'), | ||||
|                                                 context); | ||||
| @@ -327,7 +301,7 @@ class _AppPageState extends State<AppPage> { | ||||
|                                         () async { | ||||
|                                           if (app?.app.additionalSettings[ | ||||
|                                                   'trackOnly'] != | ||||
|                                               'true') { | ||||
|                                               true) { | ||||
|                                             await settingsProvider | ||||
|                                                 .getInstallPermission(); | ||||
|                                           } | ||||
|   | ||||
| @@ -142,16 +142,14 @@ class AppsPageState extends State<AppsPage> { | ||||
|  | ||||
|     List<String> trackOnlyUpdateIdsAllOrSelected = []; | ||||
|     existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) { | ||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == | ||||
|           'true') { | ||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) { | ||||
|         trackOnlyUpdateIdsAllOrSelected.add(id); | ||||
|         return false; | ||||
|       } | ||||
|       return true; | ||||
|     }).toList(); | ||||
|     newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) { | ||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == | ||||
|           'true') { | ||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) { | ||||
|         trackOnlyUpdateIdsAllOrSelected.add(id); | ||||
|         return false; | ||||
|       } | ||||
| @@ -286,7 +284,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                   SizedBox( | ||||
|                                       width: 100, | ||||
|                                       child: Text( | ||||
|                                         '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}', | ||||
|                                         '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}', | ||||
|                                         overflow: TextOverflow.fade, | ||||
|                                         textAlign: TextAlign.end, | ||||
|                                       )), | ||||
| @@ -310,7 +308,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                                   .areDownloadsRunning() | ||||
|                                               ? Text(tr('pleaseWait')) | ||||
|                                               : Text( | ||||
|                                                   '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}', | ||||
|                                                   '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}', | ||||
|                                                   style: TextStyle( | ||||
|                                                       fontStyle: | ||||
|                                                           FontStyle.italic, | ||||
| @@ -366,7 +364,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|                     : IconButton( | ||||
|                         visualDensity: VisualDensity.compact, | ||||
|                         onPressed: () { | ||||
|                           showDialog<Map<String, String>?>( | ||||
|                           showDialog<Map<String, dynamic>?>( | ||||
|                               context: context, | ||||
|                               builder: (BuildContext ctx) { | ||||
|                                 return GeneratedFormModal( | ||||
| @@ -400,40 +398,33 @@ class AppsPageState extends State<AppsPage> { | ||||
|                             HapticFeedback.heavyImpact(); | ||||
|                             List<GeneratedFormItem> formItems = []; | ||||
|                             if (existingUpdateIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormItem('updates', | ||||
|                               formItems.add(GeneratedFormSwitch('updates', | ||||
|                                   label: tr('updateX', args: [ | ||||
|                                     plural('apps', | ||||
|                                         existingUpdateIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   type: FormItemType.bool, | ||||
|                                   defaultValue: 'true')); | ||||
|                                   defaultValue: true)); | ||||
|                             } | ||||
|                             if (newInstallIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormItem('installs', | ||||
|                               formItems.add(GeneratedFormSwitch('installs', | ||||
|                                   label: tr('installX', args: [ | ||||
|                                     plural('apps', | ||||
|                                         newInstallIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   type: FormItemType.bool, | ||||
|                                   defaultValue: | ||||
|                                       existingUpdateIdsAllOrSelected.isNotEmpty | ||||
|                                           ? 'true' | ||||
|                                           : '')); | ||||
|                                   defaultValue: existingUpdateIdsAllOrSelected | ||||
|                                       .isNotEmpty)); | ||||
|                             } | ||||
|                             if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { | ||||
|                               formItems.add(GeneratedFormItem('trackonlies', | ||||
|                               formItems.add(GeneratedFormSwitch('trackonlies', | ||||
|                                   label: tr('markXTrackOnlyAsUpdated', args: [ | ||||
|                                     plural('apps', | ||||
|                                         trackOnlyUpdateIdsAllOrSelected.length) | ||||
|                                   ]), | ||||
|                                   type: FormItemType.bool, | ||||
|                                   defaultValue: existingUpdateIdsAllOrSelected | ||||
|                                           .isNotEmpty || | ||||
|                                           newInstallIdsAllOrSelected.isNotEmpty | ||||
|                                       ? 'true' | ||||
|                                       : '')); | ||||
|                                       newInstallIdsAllOrSelected.isNotEmpty)); | ||||
|                             } | ||||
|                             showDialog<Map<String, String>?>( | ||||
|                             showDialog<Map<String, dynamic>?>( | ||||
|                                 context: context, | ||||
|                                 builder: (BuildContext ctx) { | ||||
|                                   var totalApps = existingUpdateIdsAllOrSelected | ||||
| @@ -453,11 +444,11 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                       [formItems]); | ||||
|                                 } | ||||
|                                 bool shouldInstallUpdates = | ||||
|                                     values['updates'] == 'true'; | ||||
|                                     values['updates'] == true; | ||||
|                                 bool shouldInstallNew = | ||||
|                                     values['installs'] == 'true'; | ||||
|                                     values['installs'] == true; | ||||
|                                 bool shouldMarkTrackOnlies = | ||||
|                                     values['trackonlies'] == 'true'; | ||||
|                                     values['trackonlies'] == true; | ||||
|                                 (() async { | ||||
|                                   if (shouldInstallNew || | ||||
|                                       shouldInstallUpdates) { | ||||
| @@ -699,7 +690,7 @@ class AppsPageState extends State<AppsPage> { | ||||
|                               : FontWeight.bold), | ||||
|                     ), | ||||
|                     onPressed: () { | ||||
|                       showDialog<Map<String, String>?>( | ||||
|                       showDialog<Map<String, dynamic>?>( | ||||
|                           context: context, | ||||
|                           builder: (BuildContext ctx) { | ||||
|                             var vals = filter == null | ||||
| @@ -709,25 +700,23 @@ class AppsPageState extends State<AppsPage> { | ||||
|                                 title: tr('filterApps'), | ||||
|                                 items: [ | ||||
|                                   [ | ||||
|                                     GeneratedFormItem('appName', | ||||
|                                     GeneratedFormTextField('appName', | ||||
|                                         label: tr('appName'), | ||||
|                                         required: false, | ||||
|                                         defaultValue: vals['appName']), | ||||
|                                     GeneratedFormItem('author', | ||||
|                                     GeneratedFormTextField('author', | ||||
|                                         label: tr('author'), | ||||
|                                         required: false, | ||||
|                                         defaultValue: vals['author']) | ||||
|                                   ], | ||||
|                                   [ | ||||
|                                     GeneratedFormItem('upToDateApps', | ||||
|                                     GeneratedFormSwitch('upToDateApps', | ||||
|                                         label: tr('upToDateApps'), | ||||
|                                         type: FormItemType.bool, | ||||
|                                         defaultValue: vals['upToDateApps']) | ||||
|                                   ], | ||||
|                                   [ | ||||
|                                     GeneratedFormItem('nonInstalledApps', | ||||
|                                     GeneratedFormSwitch('nonInstalledApps', | ||||
|                                         label: tr('nonInstalledApps'), | ||||
|                                         type: FormItemType.bool, | ||||
|                                         defaultValue: vals['nonInstalledApps']) | ||||
|                                   ], | ||||
|                                   [ | ||||
| @@ -768,21 +757,21 @@ class AppsFilter { | ||||
|       this.includeNonInstalled = true, | ||||
|       this.categoryFilter = ''}); | ||||
|  | ||||
|   Map<String, String> toValuesMap() { | ||||
|   Map<String, dynamic> toValuesMap() { | ||||
|     return { | ||||
|       'appName': nameFilter, | ||||
|       'author': authorFilter, | ||||
|       'upToDateApps': includeUptodate ? 'true' : '', | ||||
|       'nonInstalledApps': includeNonInstalled ? 'true' : '', | ||||
|       'upToDateApps': includeUptodate, | ||||
|       'nonInstalledApps': includeNonInstalled, | ||||
|       'category': categoryFilter | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   AppsFilter.fromValuesMap(Map<String, String> values) { | ||||
|   AppsFilter.fromValuesMap(Map<String, dynamic> values) { | ||||
|     nameFilter = values['appName']!; | ||||
|     authorFilter = values['author']!; | ||||
|     includeUptodate = values['upToDateApps'] == 'true'; | ||||
|     includeNonInstalled = values['nonInstalledApps'] == 'true'; | ||||
|     includeUptodate = values['upToDateApps']; | ||||
|     includeNonInstalled = values['nonInstalledApps']; | ||||
|     categoryFilter = values['category']!; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -138,18 +138,19 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                           onPressed: importInProgress | ||||
|                               ? null | ||||
|                               : () { | ||||
|                                   showDialog( | ||||
|                                   showDialog<Map<String, dynamic>?>( | ||||
|                                       context: context, | ||||
|                                       builder: (BuildContext ctx) { | ||||
|                                         return GeneratedFormModal( | ||||
|                                           title: tr('importFromURLList'), | ||||
|                                           items: [ | ||||
|                                             [ | ||||
|                                               GeneratedFormItem('appURLList', | ||||
|                                               GeneratedFormTextField( | ||||
|                                                   'appURLList', | ||||
|                                                   label: tr('appURLList'), | ||||
|                                                   max: 7, | ||||
|                                                   additionalValidators: [ | ||||
|                                                     (String? value) { | ||||
|                                                     (dynamic value) { | ||||
|                                                       if (value != null && | ||||
|                                                           value.isNotEmpty) { | ||||
|                                                         var lines = value | ||||
| @@ -176,7 +177,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                       }).then((values) { | ||||
|                                     if (values != null) { | ||||
|                                       var urls = | ||||
|                                           (values[0] as String).split('\n'); | ||||
|                                           (values['appURLList'] as String) | ||||
|                                               .split('\n'); | ||||
|                                       setState(() { | ||||
|                                         importInProgress = true; | ||||
|                                       }); | ||||
| @@ -224,7 +226,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                             : () { | ||||
|                                                 () async { | ||||
|                                                   var values = await showDialog< | ||||
|                                                           List<String>>( | ||||
|                                                           Map<String, | ||||
|                                                               dynamic>?>( | ||||
|                                                       context: context, | ||||
|                                                       builder: | ||||
|                                                           (BuildContext ctx) { | ||||
| @@ -235,7 +238,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                                               ]), | ||||
|                                                           items: [ | ||||
|                                                             [ | ||||
|                                                               GeneratedFormItem( | ||||
|                                                               GeneratedFormTextField( | ||||
|                                                                   'searchQuery', | ||||
|                                                                   label: tr( | ||||
|                                                                       'searchQuery')) | ||||
| @@ -244,13 +247,17 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                                         ); | ||||
|                                                       }); | ||||
|                                                   if (values != null && | ||||
|                                                       values[0].isNotEmpty) { | ||||
|                                                       (values['searchQuery'] | ||||
|                                                                   as String?) | ||||
|                                                               ?.isNotEmpty == | ||||
|                                                           true) { | ||||
|                                                     setState(() { | ||||
|                                                       importInProgress = true; | ||||
|                                                     }); | ||||
|                                                     var urlsWithDescriptions = | ||||
|                                                         await source | ||||
|                                                             .search(values[0]); | ||||
|                                                         await source.search( | ||||
|                                                             values['searchQuery'] | ||||
|                                                                 as String); | ||||
|                                                     if (urlsWithDescriptions | ||||
|                                                         .isNotEmpty) { | ||||
|                                                       var selectedUrls = | ||||
| @@ -345,7 +352,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                                                                   .requiredArgs | ||||
|                                                                   .map( | ||||
|                                                                       (e) => [ | ||||
|                                                                             GeneratedFormItem(e, | ||||
|                                                                             GeneratedFormTextField(e, | ||||
|                                                                                 label: e) | ||||
|                                                                           ]) | ||||
|                                                                   .toList(), | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/components/generated_form_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'; | ||||
| @@ -41,7 +42,6 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|   Widget build(BuildContext context) { | ||||
|     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||
|     if (settingsProvider.prefs == null) { | ||||
|       settingsProvider.initializeSettings(); | ||||
|     } | ||||
| @@ -130,6 +130,28 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|     var localeDropdown = DropdownButtonFormField( | ||||
|         decoration: InputDecoration(labelText: tr('language')), | ||||
|         value: settingsProvider.forcedLocale, | ||||
|         items: [ | ||||
|           DropdownMenuItem( | ||||
|             value: null, | ||||
|             child: Text(tr('followSystem')), | ||||
|           ), | ||||
|           ...supportedLocales.map((e) => DropdownMenuItem( | ||||
|                 value: e.toLanguageTag(), | ||||
|                 child: Text(e.toLanguageTag().toUpperCase()), | ||||
|               )) | ||||
|         ], | ||||
|         onChanged: (value) { | ||||
|           settingsProvider.forcedLocale = value; | ||||
|           if (value != null) { | ||||
|             context.setLocale(Locale(value)); | ||||
|           } else { | ||||
|             context.resetLocale(); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|     var intervalDropdown = DropdownButtonFormField( | ||||
|         decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')), | ||||
|         value: settingsProvider.updateInterval, | ||||
| @@ -158,9 +180,10 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|     var sourceSpecificFields = sourceProvider.sources.map((e) { | ||||
|       if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) { | ||||
|         return GeneratedForm( | ||||
|             items: e.additionalSourceSpecificSettingFormItems | ||||
|                 .map((e) => [e]) | ||||
|                 .toList(), | ||||
|             items: e.additionalSourceSpecificSettingFormItems.map((e) { | ||||
|               e.defaultValue = settingsProvider.getSettingString(e.key); | ||||
|               return [e]; | ||||
|             }).toList(), | ||||
|             onValueChanges: (values, valid, isBuilding) { | ||||
|               if (valid) { | ||||
|                 values.forEach((key, value) { | ||||
| @@ -177,8 +200,6 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|       height: 16, | ||||
|     ); | ||||
|  | ||||
|     var categories = settingsProvider.categories; | ||||
|  | ||||
|     return Scaffold( | ||||
|         backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|         body: CustomScrollView(slivers: <Widget>[ | ||||
| @@ -212,6 +233,8 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             localeDropdown, | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
| @@ -254,7 +277,6 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                   color: Theme.of(context).colorScheme.primary), | ||||
|                             ), | ||||
|                             ...sourceSpecificFields, | ||||
|                             intervalDropdown, | ||||
|                             const Divider( | ||||
|                               height: 48, | ||||
|                             ), | ||||
| @@ -264,84 +286,7 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                   color: Theme.of(context).colorScheme.primary), | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Wrap( | ||||
|                               children: [ | ||||
|                                 ...categories.entries.toList().map((e) { | ||||
|                                   return Padding( | ||||
|                                       padding: const EdgeInsets.symmetric( | ||||
|                                           horizontal: 4), | ||||
|                                       child: Chip( | ||||
|                                         label: Text(e.key), | ||||
|                                         backgroundColor: Color(e.value), | ||||
|                                         visualDensity: VisualDensity.compact, | ||||
|                                         onDeleted: () { | ||||
|                                           showDialog<Map<String, String>?>( | ||||
|                                               context: context, | ||||
|                                               builder: (BuildContext ctx) { | ||||
|                                                 return GeneratedFormModal( | ||||
|                                                     title: tr( | ||||
|                                                         'deleteCategoryQuestion'), | ||||
|                                                     message: tr( | ||||
|                                                         'categoryDeleteWarning', | ||||
|                                                         args: [e.key]), | ||||
|                                                     items: []); | ||||
|                                               }).then((value) { | ||||
|                                             if (value != null) { | ||||
|                                               setState(() { | ||||
|                                                 categories.remove(e.key); | ||||
|                                                 settingsProvider.categories = | ||||
|                                                     categories; | ||||
|                                               }); | ||||
|                                               appsProvider.saveApps(appsProvider | ||||
|                                                   .apps.values | ||||
|                                                   .where((element) => | ||||
|                                                       element.app.category == | ||||
|                                                       e.key) | ||||
|                                                   .map((e) { | ||||
|                                                 var a = e.app; | ||||
|                                                 a.category = null; | ||||
|                                                 return a; | ||||
|                                               }).toList()); | ||||
|                                             } | ||||
|                                           }); | ||||
|                                         }, | ||||
|                                       )); | ||||
|                                 }), | ||||
|                                 Padding( | ||||
|                                     padding: const EdgeInsets.symmetric( | ||||
|                                         horizontal: 4), | ||||
|                                     child: IconButton( | ||||
|                                       onPressed: () { | ||||
|                                         showDialog<Map<String, String>?>( | ||||
|                                             context: context, | ||||
|                                             builder: (BuildContext ctx) { | ||||
|                                               return GeneratedFormModal( | ||||
|                                                   title: tr('addCategory'), | ||||
|                                                   items: [ | ||||
|                                                     [ | ||||
|                                                       GeneratedFormItem('label', | ||||
|                                                           label: tr('label')) | ||||
|                                                     ] | ||||
|                                                   ]); | ||||
|                                             }).then((value) { | ||||
|                                           String? label = value?['label']; | ||||
|                                           if (label != null) { | ||||
|                                             setState(() { | ||||
|                                               categories[label] = | ||||
|                                                   generateRandomLightColor() | ||||
|                                                       .value; | ||||
|                                               settingsProvider.categories = | ||||
|                                                   categories; | ||||
|                                             }); | ||||
|                                           } | ||||
|                                         }); | ||||
|                                       }, | ||||
|                                       icon: const Icon(Icons.add), | ||||
|                                       visualDensity: VisualDensity.compact, | ||||
|                                       tooltip: tr('add'), | ||||
|                                     )) | ||||
|                               ], | ||||
|                             ) | ||||
|                             const CategoryEditorSelector() | ||||
|                           ], | ||||
|                         ))), | ||||
|           SliverToBoxAdapter( | ||||
| @@ -456,3 +401,59 @@ class _LogsDialogState extends State<LogsDialog> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class CategoryEditorSelector extends StatefulWidget { | ||||
|   final void Function(List<String> categories)? onSelected; | ||||
|   final bool singleSelect; | ||||
|   final Set<String> preselected; | ||||
|   final WrapAlignment alignment; | ||||
|   const CategoryEditorSelector( | ||||
|       {super.key, | ||||
|       this.onSelected, | ||||
|       this.singleSelect = false, | ||||
|       this.preselected = const {}, | ||||
|       this.alignment = WrapAlignment.start}); | ||||
|  | ||||
|   @override | ||||
|   State<CategoryEditorSelector> createState() => _CategoryEditorSelectorState(); | ||||
| } | ||||
|  | ||||
| class _CategoryEditorSelectorState extends State<CategoryEditorSelector> { | ||||
|   Map<String, MapEntry<int, bool>> storedValues = {}; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     var settingsProvider = context.watch<SettingsProvider>(); | ||||
|     storedValues = settingsProvider.categories.map((key, value) => MapEntry( | ||||
|         key, | ||||
|         MapEntry(value, | ||||
|             storedValues[key]?.value ?? widget.preselected.contains(key)))); | ||||
|     return GeneratedForm( | ||||
|         items: [ | ||||
|           [ | ||||
|             GeneratedFormTagInput('categories', | ||||
|                 label: tr('category'), | ||||
|                 emptyMessage: tr('noCategories'), | ||||
|                 defaultValue: storedValues, | ||||
|                 alignment: widget.alignment, | ||||
|                 deleteConfirmationMessage: MapEntry( | ||||
|                     tr('deleteCategoriesQuestion'), | ||||
|                     tr('categoryDeleteWarning')), | ||||
|                 singleSelect: widget.singleSelect) | ||||
|           ] | ||||
|         ], | ||||
|         onValueChanges: ((values, valid, isBuilding) { | ||||
|           if (!isBuilding) { | ||||
|             storedValues = | ||||
|                 values['categories'] as Map<String, MapEntry<int, bool>>; | ||||
|             settingsProvider.categories = | ||||
|                 storedValues.map((key, value) => MapEntry(key, value.key)); | ||||
|             if (widget.onSelected != null) { | ||||
|               widget.onSelected!(storedValues.keys | ||||
|                   .where((k) => storedValues[k]!.value) | ||||
|                   .toList()); | ||||
|             } | ||||
|           } | ||||
|         })); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -313,7 +313,7 @@ class AppsProvider with ChangeNotifier { | ||||
|         throw ObtainiumError(tr('appNotFound')); | ||||
|       } | ||||
|       String? apkUrl; | ||||
|       var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == 'true'; | ||||
|       var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true; | ||||
|       if (!trackOnly) { | ||||
|         apkUrl = await confirmApkUrl(apps[id]!.app, context); | ||||
|       } | ||||
| @@ -452,9 +452,9 @@ class AppsProvider with ChangeNotifier { | ||||
|   // Don't save changes, just return the object if changes were made (else null) | ||||
|   App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { | ||||
|     var modded = false; | ||||
|     var trackOnly = app.additionalSettings['trackOnly'] == 'true'; | ||||
|     var trackOnly = app.additionalSettings['trackOnly'] == true; | ||||
|     var noVersionDetection = | ||||
|         app.additionalSettings['noVersionDetection'] == 'true'; | ||||
|         app.additionalSettings['noVersionDetection'] == true; | ||||
|     if (installedInfo == null && app.installedVersion != null && !trackOnly) { | ||||
|       app.installedVersion = null; | ||||
|       modded = true; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| @@ -153,14 +154,35 @@ class SettingsProvider with ChangeNotifier { | ||||
|  | ||||
|   set categories(Map<String, int> cats) { | ||||
|     prefs?.setString('categories', jsonEncode(cats)); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   getCategoryFormItem({String initCategory = ''}) => | ||||
|       GeneratedFormItem('category', | ||||
|   getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown( | ||||
|       'category', | ||||
|       label: tr('category'), | ||||
|           opts: [ | ||||
|       [ | ||||
|         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 | ||||
|             .where((element) => element.toLanguageTag() == fl) | ||||
|             .isNotEmpty | ||||
|         ? fl | ||||
|         : null; | ||||
|   } | ||||
|  | ||||
|   set forcedLocale(String? fl) { | ||||
|     if (fl == null) { | ||||
|       prefs?.remove('forcedLocale'); | ||||
|     } else if (supportedLocales | ||||
|         .where((element) => element.toLanguageTag() == fl) | ||||
|         .isNotEmpty) { | ||||
|       prefs?.setString('forcedLocale', fl); | ||||
|     } | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -45,7 +45,7 @@ class App { | ||||
|   late String latestVersion; | ||||
|   List<String> apkUrls = []; | ||||
|   late int preferredApkIndex; | ||||
|   late Map<String, String> additionalSettings; | ||||
|   late Map<String, dynamic> additionalSettings; | ||||
|   late DateTime? lastUpdateCheck; | ||||
|   bool pinned = false; | ||||
|   String? category; | ||||
| @@ -72,24 +72,36 @@ class App { | ||||
|     var source = SourceProvider().getSource(json['url']); | ||||
|     var formItems = source.combinedAppSpecificSettingFormItems | ||||
|         .reduce((value, element) => [...value, ...element]); | ||||
|     Map<String, String> additionalSettings = | ||||
|     Map<String, dynamic> additionalSettings = | ||||
|         getDefaultValuesFromFormItems([formItems]); | ||||
|     if (json['additionalSettings'] != null) { | ||||
|       additionalSettings.addEntries( | ||||
|           Map<String, String>.from(jsonDecode(json['additionalSettings'])) | ||||
|           Map<String, dynamic>.from(jsonDecode(json['additionalSettings'])) | ||||
|               .entries); | ||||
|     } | ||||
|     // If needed, migrate old-style additionalData to new-style additionalSettings | ||||
|     // If needed, migrate old-style additionalData to newer-style additionalSettings (V1) | ||||
|     if (json['additionalData'] != null) { | ||||
|       List<String> temp = List<String>.from(jsonDecode(json['additionalData'])); | ||||
|       temp.asMap().forEach((i, value) { | ||||
|         if (i < formItems.length) { | ||||
|           if (formItems[i] is GeneratedFormSwitch) { | ||||
|             additionalSettings[formItems[i].key] = value == 'true'; | ||||
|           } else { | ||||
|             additionalSettings[formItems[i].key] = value; | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       additionalSettings['trackOnly'] = (json['trackOnly'] ?? false).toString(); | ||||
|       additionalSettings['trackOnly'] = | ||||
|           json['trackOnly'] == 'true' || json['trackOnly'] == true; | ||||
|       additionalSettings['noVersionDetection'] = | ||||
|           (json['noVersionDetection'] ?? false).toString(); | ||||
|           json['noVersionDetection'] == 'true' || json['trackOnly'] == true; | ||||
|     } | ||||
|     // Ensure additionalSettings are correctly typed | ||||
|     for (var item in formItems) { | ||||
|       if (additionalSettings[item.key] != null) { | ||||
|         additionalSettings[item.key] = | ||||
|             item.ensureType(additionalSettings[item.key]); | ||||
|       } | ||||
|     } | ||||
|     return App( | ||||
|         json['id'] as String, | ||||
| @@ -160,7 +172,7 @@ List<String> getLinksFromParsedHTML( | ||||
|         .map((e) => '$prependToLinks${e.attributes['href']!}') | ||||
|         .toList(); | ||||
|  | ||||
| Map<String, String> getDefaultValuesFromFormItems( | ||||
| Map<String, dynamic> getDefaultValuesFromFormItems( | ||||
|     List<List<GeneratedFormItem>> items) { | ||||
|   return Map.fromEntries(items | ||||
|       .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) | ||||
| @@ -181,7 +193,7 @@ class AppSource { | ||||
|   } | ||||
|  | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|       String standardUrl, Map<String, String> additionalSettings) { | ||||
|       String standardUrl, Map<String, dynamic> additionalSettings) { | ||||
|     throw NotImplementedError(); | ||||
|   } | ||||
|  | ||||
| @@ -193,16 +205,12 @@ class AppSource { | ||||
|   final List<List<GeneratedFormItem>> | ||||
|       additionalAppSpecificSourceAgnosticSettingFormItems = [ | ||||
|     [ | ||||
|       GeneratedFormItem( | ||||
|       GeneratedFormSwitch( | ||||
|         'trackOnly', | ||||
|         label: tr('trackOnly'), | ||||
|         type: FormItemType.bool, | ||||
|       ) | ||||
|     ], | ||||
|     [ | ||||
|       GeneratedFormItem('noVersionDetection', | ||||
|           label: tr('noVersionDetection'), type: FormItemType.bool) | ||||
|     ] | ||||
|     [GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))] | ||||
|   ]; | ||||
|  | ||||
|   // Previous 2 variables combined into one at runtime for convenient usage | ||||
| @@ -230,7 +238,7 @@ class AppSource { | ||||
|   } | ||||
|  | ||||
|   String? tryInferringAppId(String standardUrl, | ||||
|       {Map<String, String> additionalSettings = const {}}) { | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| @@ -293,7 +301,7 @@ class SourceProvider { | ||||
|   bool ifRequiredAppSpecificSettingsExist(AppSource source) { | ||||
|     for (var row in source.combinedAppSpecificSettingFormItems) { | ||||
|       for (var element in row) { | ||||
|         if (element.required && element.opts == null) { | ||||
|         if (element is GeneratedFormTextField && element.required) { | ||||
|           return true; | ||||
|         } | ||||
|       } | ||||
| @@ -319,17 +327,17 @@ class SourceProvider { | ||||
|   } | ||||
|  | ||||
|   Future<App> getApp( | ||||
|       AppSource source, String url, Map<String, String> additionalSettings, | ||||
|       AppSource source, String url, Map<String, dynamic> additionalSettings, | ||||
|       {App? currentApp, | ||||
|       bool trackOnlyOverride = false, | ||||
|       noVersionDetectionOverride = false}) async { | ||||
|     if (trackOnlyOverride) { | ||||
|       additionalSettings['trackOnly'] = 'true'; | ||||
|       additionalSettings['trackOnly'] = true; | ||||
|     } | ||||
|     if (noVersionDetectionOverride) { | ||||
|       additionalSettings['noVersionDetection'] = 'true'; | ||||
|       additionalSettings['noVersionDetection'] = true; | ||||
|     } | ||||
|     var trackOnly = currentApp?.additionalSettings['trackOnly'] == 'true'; | ||||
|     var trackOnly = currentApp?.additionalSettings['trackOnly'] == true; | ||||
|     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); | ||||
|     APKDetails apk = | ||||
|         await source.getLatestAPKDetails(standardUrl, additionalSettings); | ||||
|   | ||||
| @@ -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.0+88 # When changing this, update the tag in main() accordingly | ||||
| version: 0.9.5+93 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user