mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-29 04:23:29 +01:00 
			
		
		
		
	Compare commits
	
		
			63 Commits
		
	
	
		
			v0.8.23-be
			...
			v0.9.12-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0a93117bf0 | ||
|  | 451cc41c45 | ||
|  | 3b449d0982 | ||
|  | 1863f55372 | ||
|  | 0c4b8ac79d | ||
|  | e287087753 | ||
|  | 82bcc46d42 | ||
|  | 1f26188ec6 | ||
|  | 794c3e1a81 | ||
|  | 16369b4adf | ||
|  | 8f16f745be | ||
|  | 8ddeb3d776 | ||
|  | 21cf9c98d9 | ||
|  | 358f910d19 | ||
|  | 7a3d74bd05 | ||
|  | 6f27f64699 | ||
|  | 3341fecb68 | ||
|  | d3bce63ca4 | ||
|  | 8aa8b6b698 | ||
|  | 3d6c9bbf98 | ||
|  | 7af0a8628c | ||
|  | 4573ce6bcf | ||
|  | e29d38fa32 | ||
|  | dc82431235 | ||
|  | 424b0028bf | ||
|  | 46fba9e0a4 | ||
|  | b40be7569b | ||
|  | a173be11eb | ||
|  | 0c97b25d99 | ||
|  | f836fd20d8 | ||
|  | 2f6917592d | ||
|  | b864fef3ad | ||
|  | 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 | ||
|  | 070b6033bd | ||
|  | 626bebbe5a | ||
|  | 118460ccb9 | ||
|  | 26f953dbb0 | ||
|  | 99d7595f2d | 
| @@ -1,4 +1,4 @@ | |||||||
| #  Obtainium | #  Obtainium | ||||||
|  |  | ||||||
| Get Android App Updates Directly From the Source. | Get Android App Updates Directly From the Source. | ||||||
|  |  | ||||||
| @@ -15,7 +15,8 @@ Currently supported App sources: | |||||||
| - [Signal](https://signal.org/) | - [Signal](https://signal.org/) | ||||||
| - [SourceForge](https://sourceforge.net/) | - [SourceForge](https://sourceforge.net/) | ||||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | - [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) | - [Steam](https://store.steampowered.com/mobile) | ||||||
|  |  | ||||||
| ## Limitations | ## Limitations | ||||||
|   | |||||||
| @@ -51,4 +51,7 @@ | |||||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> |     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> |     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||||
|     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> |     <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> | ||||||
|  |     <uses-permission | ||||||
|  |         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||||
|  |         android:maxSdkVersion="28"/> | ||||||
| </manifest> | </manifest> | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.1 KiB | 
| @@ -135,7 +135,7 @@ | |||||||
|     "appearance": "Aussehen", |     "appearance": "Aussehen", | ||||||
|     "showWebInAppView": "Quellwebseite in der App-Ansicht anzeigen", |     "showWebInAppView": "Quellwebseite in der App-Ansicht anzeigen", | ||||||
|     "pinUpdates": "Apps mit Aktualisierungen oben anheften", |     "pinUpdates": "Apps mit Aktualisierungen oben anheften", | ||||||
|     "updates": "Aktualisiert", |     "updates": "Aktualisierungen", | ||||||
|     "sourceSpecific": "Quellenspezifisch", |     "sourceSpecific": "Quellenspezifisch", | ||||||
|     "appSource": "App-Quelle", |     "appSource": "App-Quelle", | ||||||
|     "noLogs": "Keine Protokolle", |     "noLogs": "Keine Protokolle", | ||||||
| @@ -188,17 +188,29 @@ | |||||||
|     "steam": "Steam", |     "steam": "Steam", | ||||||
|     "steamMobile": "Steam Mobile", |     "steamMobile": "Steam Mobile", | ||||||
|     "steamChat": "Steam Chat", |     "steamChat": "Steam Chat", | ||||||
|     "install": "Install", |     "install": "Installieren", | ||||||
|     "markInstalled": "Mark Installed", |     "markInstalled": "Als Installiert markieren", | ||||||
|     "update": "Update", |     "update": "Aktualisieren", | ||||||
|     "markUpdated": "Mark Updated", |     "markUpdated": "Als Aktuell markieren", | ||||||
|     "additionalOptions": "Additional Options", |     "additionalOptions": "Zusätzliche Optionen", | ||||||
|     "disableVersionDetection": "Disable Version Detection", |     "disableVersionDetection": "Versionsermittlung deaktivieren", | ||||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", |     "noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.", | ||||||
|     "downloadingX": "Downloading {}", |     "downloadingX": "Lade {} herunter", | ||||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", |     "downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App", | ||||||
|     "noAPKFound": "No APK found", |     "noAPKFound": "Keine APK gefunden", | ||||||
|     "noVersionDetection": "No version detection", |     "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", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", |         "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", | ||||||
|         "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" |         "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" | ||||||
|   | |||||||
| @@ -135,7 +135,7 @@ | |||||||
|     "appearance": "Appearance", |     "appearance": "Appearance", | ||||||
|     "showWebInAppView": "Show Source Webpage in App View", |     "showWebInAppView": "Show Source Webpage in App View", | ||||||
|     "pinUpdates": "Pin Updates to Top of Apps View", |     "pinUpdates": "Pin Updates to Top of Apps View", | ||||||
|     "updates": "Updated", |     "updates": "Updates", | ||||||
|     "sourceSpecific": "Source-Specific", |     "sourceSpecific": "Source-Specific", | ||||||
|     "appSource": "App Source", |     "appSource": "App Source", | ||||||
|     "noLogs": "No Logs", |     "noLogs": "No Logs", | ||||||
| @@ -199,6 +199,18 @@ | |||||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", |     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", | ||||||
|     "noAPKFound": "No APK found", |     "noAPKFound": "No APK found", | ||||||
|     "noVersionDetection": "No version detection", |     "noVersionDetection": "No version detection", | ||||||
|  |     "categorize": "Categorize", | ||||||
|  |     "categories": "Categories", | ||||||
|  |     "category": "Category", | ||||||
|  |     "noCategory": "No Category", | ||||||
|  |     "noCategories": "No Categories", | ||||||
|  |     "deleteCategoriesQuestion": "Delete Categories?", | ||||||
|  |     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||||
|  |     "addCategory": "Add Category", | ||||||
|  |     "label": "Label", | ||||||
|  |     "language": "Language", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "Too many requests (rate limited) - try again in {} minute", |         "one": "Too many requests (rate limited) - try again in {} minute", | ||||||
|         "other": "Too many requests (rate limited) - try again in {} minutes" |         "other": "Too many requests (rate limited) - try again in {} minutes" | ||||||
|   | |||||||
| @@ -13,10 +13,10 @@ | |||||||
|     "and": "és", |     "and": "és", | ||||||
|     "startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva", |     "startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva", | ||||||
|     "bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}", |     "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", |     "bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött", | ||||||
|     "firstRun": "Ez az Obtainium első futása", |     "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)", |     "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", |     "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", | ||||||
|     "githubPATFormat": "felhasználónév:token", |     "githubPATFormat": "felhasználónév:token", | ||||||
| @@ -28,13 +28,13 @@ | |||||||
|     "noDescription": "Nincs leírás", |     "noDescription": "Nincs leírás", | ||||||
|     "cancel": "Mégse", |     "cancel": "Mégse", | ||||||
|     "continue": "Tovább", |     "continue": "Tovább", | ||||||
|     "requiredInBrackets": "(Kötlező)", |     "requiredInBrackets": "(Kötelező)", | ||||||
|     "dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓBAN KELL RENDELNI", |     "dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI", | ||||||
|     "colour": "Szín", |     "colour": "Szín", | ||||||
|     "githubStarredRepos": "GitHub Csillagozott Repo-k", |     "githubStarredRepos": "GitHub Csillagos Repo-k", | ||||||
|     "uname": "Felh.név", |     "uname": "Felh.név", | ||||||
|     "wrongArgNum": "Rossz számú argumentumot adott meg", |     "wrongArgNum": "Rossz számú argumentumot adott meg", | ||||||
|     "xIsTrackOnly": "{} csak nyomon követhető", |     "xIsTrackOnly": "A(z) {} csak nyomkövethető", | ||||||
|     "source": "Forrás", |     "source": "Forrás", | ||||||
|     "app": "App", |     "app": "App", | ||||||
|     "appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.", |     "appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.", | ||||||
| @@ -56,25 +56,25 @@ | |||||||
|     "appsString": "Appok", |     "appsString": "Appok", | ||||||
|     "noApps": "Nincs App", |     "noApps": "Nincs App", | ||||||
|     "noAppsForFilter": "Nincsenek appok a szűrőhöz", |     "noAppsForFilter": "Nincsenek appok a szűrőhöz", | ||||||
|     "byX": "By {}", |     "byX": "{} által", | ||||||
|     "percentProgress": "Folyamat: {}%", |     "percentProgress": "Folyamat: {}%", | ||||||
|     "pleaseWait": "Kis türelmet", |     "pleaseWait": "Kis türelmet", | ||||||
|     "updateAvailable": "Frissítés elérhető", |     "updateAvailable": "Frissítés érhető el", | ||||||
|     "estimateInBracketsShort": "(Becsült)", |     "estimateInBracketsShort": "(Becsült)", | ||||||
|     "notInstalled": "Nem telepített", |     "notInstalled": "Nem telepített", | ||||||
|     "estimateInBrackets": "(Becslés)", |     "estimateInBrackets": "(Becslés)", | ||||||
|     "selectAll": "Mindet kiválaszt", |     "selectAll": "Mindet kiválaszt", | ||||||
|     "deselectN": "Törölje {} kijelölését", |     "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?", |     "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?", | ||||||
|     "removeSelectedApps": "Távolítsa el a kiválasztott appokat", |     "removeSelectedApps": "Távolítsa el a kiválasztott appokat", | ||||||
|     "updateX": "Frissítés: {}", |     "updateX": "Frissítés: {}", | ||||||
|     "installX": "Telepítés {}", |     "installX": "Telepítés: {}", | ||||||
|     "markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nas Frissítve", |     "markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nmint Frissített", | ||||||
|     "changeX": "Változás {}", |     "changeX": "Változás {}", | ||||||
|     "installUpdateApps": "Appok telepítése/frissítése", |     "installUpdateApps": "Appok telepítése/frissítése", | ||||||
|     "installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat", |     "installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat", | ||||||
|     "onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető automatikusan (nem gyakori).", |     "onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető autom. (nem gyakori).", | ||||||
|     "markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?", |     "markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?", | ||||||
|     "no": "Nem", |     "no": "Nem", | ||||||
|     "yes": "Igen", |     "yes": "Igen", | ||||||
| @@ -86,8 +86,8 @@ | |||||||
|     "shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit", |     "shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit", | ||||||
|     "resetInstallStatus": "Telepítési állapot visszaállítása", |     "resetInstallStatus": "Telepítési állapot visszaállítása", | ||||||
|     "more": "További", |     "more": "További", | ||||||
|     "removeOutdatedFilter": "Távolítsa el az elavult alkalmazásszűrőt", |     "removeOutdatedFilter": "Távolítsa el az elavult app szűrőt", | ||||||
|     "showOutdatedOnly": "Csak az elavult alkalmazások megjelenítése", |     "showOutdatedOnly": "Csak az elavult appok megjelenítése", | ||||||
|     "filter": "Szűrő", |     "filter": "Szűrő", | ||||||
|     "filterActive": "Szűrő *", |     "filterActive": "Szűrő *", | ||||||
|     "filterApps": "Appok szűrése", |     "filterApps": "Appok szűrése", | ||||||
| @@ -118,7 +118,7 @@ | |||||||
|     "selectURLs": "Kiválasztott URL-ek", |     "selectURLs": "Kiválasztott URL-ek", | ||||||
|     "pick": "Válasszon", |     "pick": "Válasszon", | ||||||
|     "theme": "Téma", |     "theme": "Téma", | ||||||
|     "dark": "Söét", |     "dark": "Sötét", | ||||||
|     "light": "Világos", |     "light": "Világos", | ||||||
|     "followSystem": "Rendszer szerint", |     "followSystem": "Rendszer szerint", | ||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
| @@ -126,16 +126,16 @@ | |||||||
|     "appSortBy": "App rendezés...", |     "appSortBy": "App rendezés...", | ||||||
|     "authorName": "Szerző/Név", |     "authorName": "Szerző/Név", | ||||||
|     "nameAuthor": "Név/Szerző", |     "nameAuthor": "Név/Szerző", | ||||||
|     "asAdded": "Mint hozzáadott", |     "asAdded": "Mint Hozzáadott", | ||||||
|     "appSortOrder": "Appok rendezése", |     "appSortOrder": "Appok rendezése", | ||||||
|     "ascending": "Emelkedő", |     "ascending": "Emelkedő", | ||||||
|     "descending": "Csökkenő", |     "descending": "Csökkenő", | ||||||
|     "bgUpdateCheckInterval": "Háttérfrissítés ellenőrzési időköz", |     "bgUpdateCheckInterval": "Háttérfrissítés ellenőrzés időköze", | ||||||
|     "neverManualOnly": "Soha – csak manuális", |     "neverManualOnly": "Soha – csak manuális", | ||||||
|     "appearance": "Megjelenés", |     "appearance": "Megjelenés", | ||||||
|     "showWebInAppView": "Forrás megjelenítése az Appok nézetben", |     "showWebInAppView": "Forrás megjelenítése az Appok nézetben", | ||||||
|     "pinUpdates": "Frissítések kitűzése az App nézet tetejére", |     "pinUpdates": "Frissítések kitűzése az App nézet tetejére", | ||||||
|     "updates": "Frissítve", |     "updates": "Frissítések", | ||||||
|     "sourceSpecific": "Forrás-specifikus", |     "sourceSpecific": "Forrás-specifikus", | ||||||
|     "appSource": "App forrás", |     "appSource": "App forrás", | ||||||
|     "noLogs": "Nincsenek naplók", |     "noLogs": "Nincsenek naplók", | ||||||
| @@ -145,7 +145,7 @@ | |||||||
|     "appNotFound": "App nem található", |     "appNotFound": "App nem található", | ||||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", |     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||||
|     "pickAnAPK": "Válasszon egy APK-t", |     "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.", |     "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", | ||||||
|     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", |     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", | ||||||
|     "warning": "Figyelem", |     "warning": "Figyelem", | ||||||
| @@ -153,16 +153,16 @@ | |||||||
|     "updatesAvailable": "Frissítések érhetők el", |     "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", |     "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.", |     "noNewUpdates": "Nincsenek új frissítések.", | ||||||
|     "xHasAnUpdate": "{} frissítést kapott.", |     "xHasAnUpdate": "A(z) {} frissítést kapott.", | ||||||
|     "appsUpdated": "Alkalmazások frissítve", |     "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", |     "appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy/több app frissítése megtörtént a háttérben", | ||||||
|     "xWasUpdatedToY": "{} frissítve a következőre: {}.", |     "xWasUpdatedToY": "{} frissítve a következőre: {}.", | ||||||
|     "errorCheckingUpdates": "Hiba a frissítések keresésekor", |     "errorCheckingUpdates": "Hiba a frissítések keresésekor", | ||||||
|     "errorCheckingUpdatesNotifDescription": "Értesítés, amely akkor jelenik meg, ha a háttérbeli frissítések ellenőrzése sikertelen", |     "errorCheckingUpdatesNotifDescription": "Értesítés, amely akkor jelenik meg, ha a háttérbeli frissítések ellenőrzése sikertelen", | ||||||
|     "appsRemoved": "Alkalmazások eltávolítva", |     "appsRemoved": "Alkalmazások eltávolítva", | ||||||
|     "appsRemovedNotifDescription": "Értesíti a felhasználót egy vagy több alkalmazás eltávolításáról a betöltésük során fellépő hibák miatt", |     "appsRemovedNotifDescription": "Értesíti a felhasználót egy vagy több alkalmazás eltávolításáról a betöltésük során fellépő hibák miatt", | ||||||
|     "xWasRemovedDueToErrorY": "A(z) {} a következő hiba miatt lett eltávolítva: {}", |     "xWasRemovedDueToErrorY": "A(z) {} a következő hiba miatt lett eltávolítva: {}", | ||||||
|     "completeAppInstallation": "Teljes alkalmazástelepítés", |     "completeAppInstallation": "Teljes app telepítés", | ||||||
|     "obtainiumMustBeOpenToInstallApps": "Az Obtainiumnak megnyitva kell lennie az alkalmazások telepítéséhez", |     "obtainiumMustBeOpenToInstallApps": "Az Obtainiumnak megnyitva kell lennie az alkalmazások telepítéséhez", | ||||||
|     "completeAppInstallationNotifDescription": "Megkéri a felhasználót, hogy térjen vissza az Obtainiumhoz, hogy befejezze az alkalmazás telepítését", |     "completeAppInstallationNotifDescription": "Megkéri a felhasználót, hogy térjen vissza az Obtainiumhoz, hogy befejezze az alkalmazás telepítését", | ||||||
|     "checkingForUpdates": "Frissítések keresése", |     "checkingForUpdates": "Frissítések keresése", | ||||||
| @@ -184,21 +184,32 @@ | |||||||
|     "appIdOrName": "App ID vagy név", |     "appIdOrName": "App ID vagy név", | ||||||
|     "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel", |     "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel", | ||||||
|     "reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak", |     "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", |     "steam": "Steam", | ||||||
|     "steamMobile": "Steam Mobile", |     "steamMobile": "Steam Mobile", | ||||||
|     "steamChat": "Steam Chat", |     "steamChat": "Steam Chat", | ||||||
|     "install": "Install", |     "install": "Telepít", | ||||||
|     "markInstalled": "Mark Installed", |     "markInstalled": "Telepítettnek jelöl", | ||||||
|     "update": "Update", |     "update": "Frissít", | ||||||
|     "markUpdated": "Mark Updated", |     "markUpdated": "Frissítettnek jelöl", | ||||||
|     "additionalOptions": "Additional Options", |     "additionalOptions": "További lehetőségek", | ||||||
|     "disableVersionDetection": "Disable Version Detection", |     "disableVersionDetection": "Verzióérzékelés letiltása", | ||||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", |     "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": "Downloading {}", |     "downloadingX": "{} letöltés", | ||||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", |     "downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról", | ||||||
|     "noAPKFound": "No APK found", |     "noAPKFound": "Nem található APK", | ||||||
|     "noVersionDetection": "No version detection", |     "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", | ||||||
|  |     "language": "Language", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva", |         "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" |         "other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva" | ||||||
| @@ -243,4 +254,4 @@ | |||||||
|         "one": "A(z) {} és 1 további alkalmazás frissítve.", |         "one": "A(z) {} és 1 további alkalmazás frissítve.", | ||||||
|         "other": "{} és további {} alkalmazás frissítve." |         "other": "{} és további {} alkalmazás frissítve." | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ | |||||||
|     "startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background", |     "startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background", | ||||||
|     "bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background", |     "bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background", | ||||||
|     "firstRun": "Questo è il primo avvio di sempre di Obtainium", |     "firstRun": "Questo è il primo avvio di sempre di Obtainium", | ||||||
|     "settingUpdateCheckIntervalTo": "Imposta l'intervallo di aggiornamento a {}", |     "settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}", | ||||||
|     "githubPATLabel": "GitHub Personal Access Token (aumenta il limite di traffico)", |     "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", | ||||||
|     "githubPATHint": "PAT deve seguire questo formato: username:token", |     "githubPATHint": "PAT deve seguire questo formato: username:token", | ||||||
|     "githubPATFormat": "username:token", |     "githubPATFormat": "username:token", | ||||||
|     "githubPATLinkText": "Informazioni su GitHub PAT", |     "githubPATLinkText": "Informazioni su GitHub PAT", | ||||||
| @@ -31,18 +31,18 @@ | |||||||
|     "requiredInBrackets": "(richiesto)", |     "requiredInBrackets": "(richiesto)", | ||||||
|     "dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE", |     "dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE", | ||||||
|     "colour": "Colore", |     "colour": "Colore", | ||||||
|     "githubStarredRepos": "i repository stellati da GitHub", |     "githubStarredRepos": "repository stellati da GitHub", | ||||||
|     "uname": "Username", |     "uname": "Username", | ||||||
|     "wrongArgNum": "Numero di argomenti forniti errato", |     "wrongArgNum": "Numero di argomenti forniti errato", | ||||||
|     "xIsTrackOnly": "{} è Solo-Monitoraggio", |     "xIsTrackOnly": "{} è in modalità Solo-Monitoraggio", | ||||||
|     "source": "Fonte", |     "source": "Fonte", | ||||||
|     "app": "App", |     "app": "App", | ||||||
|     "appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.", |     "appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.", | ||||||
|     "youPickedTrackOnly": "Hai selezionato l'opzione 'Solo-Monitoraggio'.", |     "youPickedTrackOnly": "È stata selezionata l'opzione 'Solo-Monitoraggio'.", | ||||||
|     "trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.", |     "trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.", | ||||||
|     "cancelled": "Annullato", |     "cancelled": "Annullato", | ||||||
|     "appAlreadyAdded": "App già aggiunta", |     "appAlreadyAdded": "App già aggiunta", | ||||||
|     "alreadyUpToDateQuestion": "App già aggiornata?", |     "alreadyUpToDateQuestion": "L'App è già aggiornata?", | ||||||
|     "addApp": "Aggiungi App", |     "addApp": "Aggiungi App", | ||||||
|     "appSourceURL": "URL della fonte dell'App", |     "appSourceURL": "URL della fonte dell'App", | ||||||
|     "error": "Errore", |     "error": "Errore", | ||||||
| @@ -60,20 +60,20 @@ | |||||||
|     "percentProgress": "Progresso: {}%", |     "percentProgress": "Progresso: {}%", | ||||||
|     "pleaseWait": "Attendere prego", |     "pleaseWait": "Attendere prego", | ||||||
|     "updateAvailable": "Aggiornamento disponibile", |     "updateAvailable": "Aggiornamento disponibile", | ||||||
|     "estimateInBracketsShort": "(Prev.)", |     "estimateInBracketsShort": "(prev.)", | ||||||
|     "notInstalled": "Non installato", |     "notInstalled": "Non installato", | ||||||
|     "estimateInBrackets": "(Previsto)", |     "estimateInBrackets": "(previsto)", | ||||||
|     "selectAll": "Seleziona tutto", |     "selectAll": "Seleziona tutto", | ||||||
|     "deselectN": "Deseleziona {}", |     "deselectN": "Deseleziona {}", | ||||||
|     "xWillBeRemovedButRemainInstalled": "{} sarà rimosso da Obtainium ma resterà installato sul dispositivo.", |     "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.", | ||||||
|     "removeSelectedAppsQuestion": "Rimuovere le App selezionate?", |     "removeSelectedAppsQuestion": "Rimuovere le App selezionate?", | ||||||
|     "removeSelectedApps": "Rimuovi le App selezionate", |     "removeSelectedApps": "Rimuovi le App selezionate", | ||||||
|     "updateX": "Aggiorna {}", |     "updateX": "Aggiorna {}", | ||||||
|     "installX": "Installa {}", |     "installX": "Installa {}", | ||||||
|     "markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato", |     "markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato", | ||||||
|     "changeX": "modifica {}", |     "changeX": "Modifica {}", | ||||||
|     "installUpdateApps": "Installa/Aggiorna le App", |     "installUpdateApps": "Installa/Aggiorna App", | ||||||
|     "installUpdateSelectedApps": "Installa/Aggiornale le App selezionate", |     "installUpdateSelectedApps": "Installa/Aggiorna le App selezionate", | ||||||
|     "onlyWorksWithNonEVDApps": "Funziona solo per le App il cui stato d'installazione non può essere rilevato automaticamente (inconsueto).", |     "onlyWorksWithNonEVDApps": "Funziona solo per le App il cui stato d'installazione non può essere rilevato automaticamente (inconsueto).", | ||||||
|     "markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?", |     "markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?", | ||||||
|     "no": "No", |     "no": "No", | ||||||
| @@ -95,7 +95,7 @@ | |||||||
|     "author": "Autore", |     "author": "Autore", | ||||||
|     "upToDateApps": "App aggiornate", |     "upToDateApps": "App aggiornate", | ||||||
|     "nonInstalledApps": "App non installate", |     "nonInstalledApps": "App non installate", | ||||||
|     "importExport": "Importa/Esporta", |     "importExport": "Importa - Esporta", | ||||||
|     "settings": "Impostazioni", |     "settings": "Impostazioni", | ||||||
|     "exportedTo": "Esportato in {}", |     "exportedTo": "Esportato in {}", | ||||||
|     "obtainiumExport": "Esporta da Obtainium", |     "obtainiumExport": "Esporta da Obtainium", | ||||||
| @@ -134,8 +134,8 @@ | |||||||
|     "neverManualOnly": "Mai - Solo manuale", |     "neverManualOnly": "Mai - Solo manuale", | ||||||
|     "appearance": "Aspetto", |     "appearance": "Aspetto", | ||||||
|     "showWebInAppView": "Mostra pagina web dell'App se selezionata", |     "showWebInAppView": "Mostra pagina web dell'App se selezionata", | ||||||
|     "pinUpdates": "Fissa in alto gli aggiornamenti disponibili", |     "pinUpdates": "Fissa aggiornamenti disponibili in alto", | ||||||
|     "updates": "Aggiornato", |     "updates": "Aggiornamenti", | ||||||
|     "sourceSpecific": "Specifiche per la fonte", |     "sourceSpecific": "Specifiche per la fonte", | ||||||
|     "appSource": "Sorgente dell'App", |     "appSource": "Sorgente dell'App", | ||||||
|     "noLogs": "Nessun log", |     "noLogs": "Nessun log", | ||||||
| @@ -146,21 +146,21 @@ | |||||||
|     "obtainiumExportHyphenatedLowercase": "esportazione-obtainium", |     "obtainiumExportHyphenatedLowercase": "esportazione-obtainium", | ||||||
|     "pickAnAPK": "Seleziona un APK", |     "pickAnAPK": "Seleziona un APK", | ||||||
|     "appHasMoreThanOnePackage": "{} offre più di un pacchetto:", |     "appHasMoreThanOnePackage": "{} offre più di un pacchetto:", | ||||||
|     "deviceSupportsXArch": "Il tuo dispositivo supporta l'architettura {} della CPU.", |     "deviceSupportsXArch": "Il dispositivo in uso supporta l'architettura {} della CPU.", | ||||||
|     "deviceSupportsFollowingArchs": "Il tuo dispositivo supporta le seguenti architetture della CPU:", |     "deviceSupportsFollowingArchs": "Il dispositivo in uso supporta le seguenti architetture della CPU:", | ||||||
|     "warning": "Attenzione", |     "warning": "Attenzione", | ||||||
|     "sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?", |     "sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?", | ||||||
|     "updatesAvailable": "Aggiornamenti disponibili", |     "updatesAvailable": "Aggiornamenti disponibili", | ||||||
|     "updatesAvailableNotifDescription": "Avvisa l'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium", |     "updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium", | ||||||
|     "noNewUpdates": "Nessun nuovo aggiornamento.", |     "noNewUpdates": "Nessun nuovo aggiornamento.", | ||||||
|     "xHasAnUpdate": "{} è stato aggiornato.", |     "xHasAnUpdate": "Aggiornamento disponibile per {}", | ||||||
|     "appsUpdated": "App aggiornate", |     "appsUpdated": "App aggiornate", | ||||||
|     "appsUpdatedNotifDescription": "Avvisa l'utente che una o più App sono state aggiornate in background", |     "appsUpdatedNotifDescription": "Notifica all'utente che una o più App sono state aggiornate in background", | ||||||
|     "xWasUpdatedToY": "{} è stato aggiornato a {}.", |     "xWasUpdatedToY": "{} è stato aggiornato a {}.", | ||||||
|     "errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti", |     "errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti", | ||||||
|     "errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce", |     "errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce", | ||||||
|     "appsRemoved": "App rimosse", |     "appsRemoved": "App rimosse", | ||||||
|     "appsRemovedNotifDescription": "Avvisa l'utente che una o più App sono state rimosse a causa di errori durante il caricamento", |     "appsRemovedNotifDescription": "Notifica all'utente che una o più App sono state rimosse a causa di errori durante il caricamento", | ||||||
|     "xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}", |     "xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}", | ||||||
|     "completeAppInstallation": "Completa l'installazione dell'App", |     "completeAppInstallation": "Completa l'installazione dell'App", | ||||||
|     "obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App", |     "obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App", | ||||||
| @@ -178,7 +178,7 @@ | |||||||
|     "installedVersionX": "Versione installata: {}", |     "installedVersionX": "Versione installata: {}", | ||||||
|     "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", |     "lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}", | ||||||
|     "remove": "Rimuovi", |     "remove": "Rimuovi", | ||||||
|     "removeAppQuestion": "Rimuovere App?", |     "removeAppQuestion": "Rimuovere l'App?", | ||||||
|     "yesMarkUpdated": "Sì, contrassegna come aggiornato", |     "yesMarkUpdated": "Sì, contrassegna come aggiornato", | ||||||
|     "fdroid": "F-Droid", |     "fdroid": "F-Droid", | ||||||
|     "appIdOrName": "ID o nome dell'App", |     "appIdOrName": "ID o nome dell'App", | ||||||
| @@ -188,28 +188,40 @@ | |||||||
|     "steam": "Steam", |     "steam": "Steam", | ||||||
|     "steamMobile": "Steam Mobile", |     "steamMobile": "Steam Mobile", | ||||||
|     "steamChat": "Steam Chat", |     "steamChat": "Steam Chat", | ||||||
|     "install": "Install", |     "install": "Installa", | ||||||
|     "markInstalled": "Mark Installed", |     "markInstalled": "Contrassegna come installato", | ||||||
|     "update": "Update", |     "update": "Aggiorna", | ||||||
|     "markUpdated": "Mark Updated", |     "markUpdated": "Contrassegna come aggiornato", | ||||||
|     "additionalOptions": "Additional Options", |     "additionalOptions": "Opzioni aggiuntive", | ||||||
|     "disableVersionDetection": "Disable Version Detection", |     "disableVersionDetection": "Disattiva il rilevamento della versione", | ||||||
|     "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", |     "noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le App la cui versione non viene rilevata correttamente.", | ||||||
|     "downloadingX": "Downloading {}", |     "downloadingX": "Scaricamento di {} in corso", | ||||||
|     "downloadNotifDescription": "Notifies the user of the progress in downloading an App", |     "downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'App", | ||||||
|     "noAPKFound": "No APK found", |     "noAPKFound": "Nessun APK trovato", | ||||||
|     "noVersionDetection": "No version detection", |     "noVersionDetection": "Disattiva rilevamento di versione", | ||||||
|  |     "categorize": "Aggiungi a categoria", | ||||||
|  |     "categories": "Categorie", | ||||||
|  |     "category": "Categoria", | ||||||
|  |     "noCategory": "Nessuna categoria", | ||||||
|  |     "noCategories": "Nessuna categoria", | ||||||
|  |     "deleteCategoriesQuestion": "Eliminare le categorie?", | ||||||
|  |     "categoryDeleteWarning": "Tutte le App nelle categorie eliminate saranno impostate come non categorizzate.", | ||||||
|  |     "addCategory": "Aggiungi categoria", | ||||||
|  |     "label": "Etichetta", | ||||||
|  |     "language": "Lingua", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", |         "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", | ||||||
|         "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" |         "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" | ||||||
|     }, |     }, | ||||||
|     "bgUpdateGotErrorRetryInMinutes": { |     "bgUpdateGotErrorRetryInMinutes": { | ||||||
|         "one": "Il controllo degli aggiornamenti in background ha incontrato un {}, ricontrollo tra {} minuto", |         "one": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuto", | ||||||
|         "other": "Il controllo degli aggiornamenti in background ha incontrato un {}, ricontrollo tra {} minuti" |         "other": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuti" | ||||||
|     }, |     }, | ||||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { |     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||||
|         "one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - avviserà l'utento se necessario", |         "one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - notificherà l'utente se necessario", | ||||||
|         "other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - avviserà l'utento se necessario" |         "other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - notificherà l'utente se necessario" | ||||||
|     }, |     }, | ||||||
|     "apps": { |     "apps": { | ||||||
|         "one": "{} App", |         "one": "{} App", | ||||||
| @@ -243,4 +255,4 @@ | |||||||
|         "one": "{} e un'altra App sono state aggiornate.", |         "one": "{} e un'altra App sono state aggiornate.", | ||||||
|         "other": "{} e altre {} App sono state aggiornate." |         "other": "{} e altre {} App sono state aggiornate." | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", |     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", | ||||||
|     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", |     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", | ||||||
|     "firstRun": "これがObtainiumの最初の実行です", |     "firstRun": "これがObtainiumの最初の実行です", | ||||||
|     "settingUpdateCheckIntervalTo": "更新間隔を{}に設定する", |     "settingUpdateCheckIntervalTo": "確認間隔を{}に設定する", | ||||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", |     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", |     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", | ||||||
|     "githubPATFormat": "ユーザー名:トークン", |     "githubPATFormat": "ユーザー名:トークン", | ||||||
| @@ -27,7 +27,7 @@ | |||||||
|     "invalidRegEx": "無効な正規表現", |     "invalidRegEx": "無効な正規表現", | ||||||
|     "noDescription": "説明はありません", |     "noDescription": "説明はありません", | ||||||
|     "cancel": "キャンセル", |     "cancel": "キャンセル", | ||||||
|     "continue": "続ける", |     "continue": "続行", | ||||||
|     "requiredInBrackets": "(必須)", |     "requiredInBrackets": "(必須)", | ||||||
|     "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です", |     "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です", | ||||||
|     "colour": "カラー", |     "colour": "カラー", | ||||||
| @@ -64,7 +64,7 @@ | |||||||
|     "notInstalled": "未インストール", |     "notInstalled": "未インストール", | ||||||
|     "estimateInBrackets": "(推定)", |     "estimateInBrackets": "(推定)", | ||||||
|     "selectAll": "すべて選択", |     "selectAll": "すべて選択", | ||||||
|     "deselectN": "{}件を選択解除", |     "deselectN": "{}件の選択を解除", | ||||||
|     "xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。", |     "xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。", | ||||||
|     "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", |     "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", | ||||||
|     "removeSelectedApps": "選択したアプリを削除する", |     "removeSelectedApps": "選択したアプリを削除する", | ||||||
| @@ -124,18 +124,18 @@ | |||||||
|     "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|     "materialYou": "Material You", |     "materialYou": "Material You", | ||||||
|     "appSortBy": "アプリの並び方", |     "appSortBy": "アプリの並び方", | ||||||
|     "authorName": "作者/アプリ名", |     "authorName": "作者名/アプリ名", | ||||||
|     "nameAuthor": "アプリ名/作者", |     "nameAuthor": "アプリ名/作者名", | ||||||
|     "asAdded": "追加順", |     "asAdded": "追加順", | ||||||
|     "appSortOrder": "並び順", |     "appSortOrder": "並び順", | ||||||
|     "ascending": "昇順", |     "ascending": "昇順", | ||||||
|     "descending": "降順", |     "descending": "降順", | ||||||
|     "bgUpdateCheckInterval": "バックグラウンド更新の確認間隔", |     "bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔", | ||||||
|     "neverManualOnly": "手動", |     "neverManualOnly": "手動", | ||||||
|     "appearance": "外観", |     "appearance": "外観", | ||||||
|     "showWebInAppView": "アプリビューにソースウェブページを表示する", |     "showWebInAppView": "アプリビューにソースウェブページを表示する", | ||||||
|     "pinUpdates": "アップデートがあるアプリをトップに固定する", |     "pinUpdates": "アップデートがあるアプリをトップに固定する", | ||||||
|     "updates": "更新", |     "updates": "アップデート", | ||||||
|     "sourceSpecific": "Github アクセストークン", |     "sourceSpecific": "Github アクセストークン", | ||||||
|     "appSource": "アプリのソース", |     "appSource": "アプリのソース", | ||||||
|     "noLogs": "ログはありません", |     "noLogs": "ログはありません", | ||||||
| @@ -144,7 +144,7 @@ | |||||||
|     "share": "共有", |     "share": "共有", | ||||||
|     "appNotFound": "アプリが見つかりません", |     "appNotFound": "アプリが見つかりません", | ||||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-エクスポート", |     "obtainiumExportHyphenatedLowercase": "obtainium-エクスポート", | ||||||
|     "pickAnAPK": "APKを選ぶ", |     "pickAnAPK": "APKを選択", | ||||||
|     "appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ", |     "appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ", | ||||||
|     "deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。", |     "deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。", | ||||||
|     "deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:", |     "deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:", | ||||||
| @@ -199,17 +199,29 @@ | |||||||
|     "downloadNotifDescription": "アプリのダウンロード状況を通知する", |     "downloadNotifDescription": "アプリのダウンロード状況を通知する", | ||||||
|     "noAPKFound": "APKが見つかりません", |     "noAPKFound": "APKが見つかりません", | ||||||
|     "noVersionDetection": "バージョン検出を行わない", |     "noVersionDetection": "バージョン検出を行わない", | ||||||
|  |     "categorize": "カテゴライズ", | ||||||
|  |     "categories": "カテゴリ", | ||||||
|  |     "category": "カテゴリ", | ||||||
|  |     "noCategory": "カテゴリなし", | ||||||
|  |     "noCategories": "カテゴリなし", | ||||||
|  |     "deleteCategoriesQuestion": "カテゴリを削除しますか?", | ||||||
|  |     "categoryDeleteWarning": "削除されたカテゴリ内のアプリは未分類に設定されます。", | ||||||
|  |     "addCategory": "カテゴリを追加", | ||||||
|  |     "label": "ラベル", | ||||||
|  |     "language": "言語", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", |         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" |         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||||
|     }, |     }, | ||||||
|     "bgUpdateGotErrorRetryInMinutes": { |     "bgUpdateGotErrorRetryInMinutes": { | ||||||
|         "one": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します", |         "one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します", | ||||||
|         "other": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します" |         "other": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します" | ||||||
|     }, |     }, | ||||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { |     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||||
|         "one": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します", |         "one": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します", | ||||||
|         "other": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" |         "other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" | ||||||
|     }, |     }, | ||||||
|     "apps": { |     "apps": { | ||||||
|         "one": "{}個のアプリ", |         "one": "{}個のアプリ", | ||||||
|   | |||||||
| @@ -199,6 +199,18 @@ | |||||||
|     "downloadNotifDescription": "通知用户下载进度", |     "downloadNotifDescription": "通知用户下载进度", | ||||||
|     "noAPKFound": "未找到安装包", |     "noAPKFound": "未找到安装包", | ||||||
|     "noVersionDetection": "无版本检测", |     "noVersionDetection": "无版本检测", | ||||||
|  |     "categorize": "Categorize", | ||||||
|  |     "categories": "Categories", | ||||||
|  |     "category": "Category", | ||||||
|  |     "noCategory": "No Category", | ||||||
|  |     "noCategories": "No Categories", | ||||||
|  |     "deleteCategoriesQuestion": "Delete Categories?", | ||||||
|  |     "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", | ||||||
|  |     "addCategory": "Add Category", | ||||||
|  |     "label": "Label", | ||||||
|  |     "language": "Language", | ||||||
|  |     "storagePermissionDenied": "Storage permission denied", | ||||||
|  |     "selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.", | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", |         "one": "请求过多 (API 限制) - 在 {} 分钟后重试", | ||||||
|         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" |         "other": "请求过多 (API 限制) - 在 {} 分钟后重试" | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class APKMirror extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await get(Uri.parse('$standardUrl/feed')); |     Response res = await get(Uri.parse('$standardUrl/feed')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -46,7 +46,7 @@ class APKMirror extends AppSource { | |||||||
|       } |       } | ||||||
|       return APKDetails(version, [], getAppNames(standardUrl)); |       return APKDetails(version, [], getAppNames(standardUrl)); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ class FDroid extends AppSource { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String? tryInferringAppId(String standardUrl, |   String? tryInferringAppId(String standardUrl, | ||||||
|       {Map<String, String> additionalSettings = const {}}) { |       {Map<String, dynamic> additionalSettings = const {}}) { | ||||||
|     return Uri.parse(standardUrl).pathSegments.last; |     return Uri.parse(standardUrl).pathSegments.last; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -54,14 +54,14 @@ class FDroid extends AppSource { | |||||||
|       return APKDetails(latestVersion, apkUrls, |       return APKDetails(latestVersion, apkUrls, | ||||||
|           AppNames(name, Uri.parse(standardUrl).pathSegments.last)); |           AppNames(name, Uri.parse(standardUrl).pathSegments.last)); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     String? appId = tryInferringAppId(standardUrl); |     String? appId = tryInferringAppId(standardUrl); | ||||||
|     return getAPKUrlsFromFDroidPackagesAPIResponse( |     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ class FDroidRepo extends AppSource { | |||||||
|  |  | ||||||
|     additionalSourceAppSpecificSettingFormItems = [ |     additionalSourceAppSpecificSettingFormItems = [ | ||||||
|       [ |       [ | ||||||
|         GeneratedFormItem('appIdOrName', |         GeneratedFormTextField('appIdOrName', | ||||||
|             label: tr('appIdOrName'), |             label: tr('appIdOrName'), | ||||||
|             hint: tr('reposHaveMultipleApps'), |             hint: tr('reposHaveMultipleApps'), | ||||||
|             required: true) |             required: true) | ||||||
| @@ -22,7 +22,7 @@ class FDroidRepo extends AppSource { | |||||||
|   @override |   @override | ||||||
|   String standardizeURL(String url) { |   String standardizeURL(String url) { | ||||||
|     RegExp standardUrlRegExp = |     RegExp standardUrlRegExp = | ||||||
|         RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)'); |         RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)'); | ||||||
|     RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase()); |     RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase()); | ||||||
|     if (match == null) { |     if (match == null) { | ||||||
|       throw InvalidURLError(name); |       throw InvalidURLError(name); | ||||||
| @@ -33,7 +33,7 @@ class FDroidRepo extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     String? appIdOrName = additionalSettings['appIdOrName']; |     String? appIdOrName = additionalSettings['appIdOrName']; | ||||||
|     if (appIdOrName == null) { |     if (appIdOrName == null) { | ||||||
| @@ -80,7 +80,7 @@ class FDroidRepo extends AppSource { | |||||||
|           .toList(); |           .toList(); | ||||||
|       return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName)); |       return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName)); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ class GitHub extends AppSource { | |||||||
|     host = 'github.com'; |     host = 'github.com'; | ||||||
|  |  | ||||||
|     additionalSourceSpecificSettingFormItems = [ |     additionalSourceSpecificSettingFormItems = [ | ||||||
|       GeneratedFormItem('github-creds', |       GeneratedFormTextField('github-creds', | ||||||
|           label: tr('githubPATLabel'), |           label: tr('githubPATLabel'), | ||||||
|           required: false, |           required: false, | ||||||
|           additionalValidators: [ |           additionalValidators: [ | ||||||
| @@ -51,21 +51,16 @@ class GitHub extends AppSource { | |||||||
|  |  | ||||||
|     additionalSourceAppSpecificSettingFormItems = [ |     additionalSourceAppSpecificSettingFormItems = [ | ||||||
|       [ |       [ | ||||||
|         GeneratedFormItem('includePrereleases', |         GeneratedFormSwitch('includePrereleases', | ||||||
|             label: tr('includePrereleases'), |             label: tr('includePrereleases'), defaultValue: false) | ||||||
|             type: FormItemType.bool, |  | ||||||
|             defaultValue: '') |  | ||||||
|       ], |       ], | ||||||
|       [ |       [ | ||||||
|         GeneratedFormItem('fallbackToOlderReleases', |         GeneratedFormSwitch('fallbackToOlderReleases', | ||||||
|             label: tr('fallbackToOlderReleases'), |             label: tr('fallbackToOlderReleases'), defaultValue: true) | ||||||
|             type: FormItemType.bool, |  | ||||||
|             defaultValue: 'true') |  | ||||||
|       ], |       ], | ||||||
|       [ |       [ | ||||||
|         GeneratedFormItem('filterReleaseTitlesByRegEx', |         GeneratedFormTextField('filterReleaseTitlesByRegEx', | ||||||
|             label: tr('filterReleaseTitlesByRegEx'), |             label: tr('filterReleaseTitlesByRegEx'), | ||||||
|             type: FormItemType.string, |  | ||||||
|             required: false, |             required: false, | ||||||
|             additionalValidators: [ |             additionalValidators: [ | ||||||
|               (value) { |               (value) { | ||||||
| @@ -111,13 +106,15 @@ class GitHub extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     var includePrereleases = additionalSettings['includePrereleases'] == 'true'; |     bool includePrereleases = additionalSettings['includePrereleases']; | ||||||
|     var fallbackToOlderReleases = |     bool fallbackToOlderReleases = | ||||||
|         additionalSettings['fallbackToOlderReleases'] == 'true'; |         additionalSettings['fallbackToOlderReleases']; | ||||||
|     var regexFilter = |     String? regexFilter = | ||||||
|         additionalSettings['filterReleaseTitlesByRegEx']?.isNotEmpty == true |         (additionalSettings['filterReleaseTitlesByRegEx'] as String?) | ||||||
|  |                     ?.isNotEmpty == | ||||||
|  |                 true | ||||||
|             ? additionalSettings['filterReleaseTitlesByRegEx'] |             ? additionalSettings['filterReleaseTitlesByRegEx'] | ||||||
|             : null; |             : null; | ||||||
|     Response res = await get(Uri.parse( |     Response res = await get(Uri.parse( | ||||||
| @@ -150,7 +147,7 @@ class GitHub extends AppSource { | |||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
|         var apkUrls = getReleaseAPKUrls(releases[i]); |         var apkUrls = getReleaseAPKUrls(releases[i]); | ||||||
|         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != 'true') { |         if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { | ||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
|         targetRelease = releases[i]; |         targetRelease = releases[i]; | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class GitLab extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); |     Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -59,7 +59,7 @@ class GitLab extends AppSource { | |||||||
|       } |       } | ||||||
|       return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl)); |       return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl)); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,14 +23,14 @@ class IzzyOnDroid extends AppSource { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String? tryInferringAppId(String standardUrl, |   String? tryInferringAppId(String standardUrl, | ||||||
|       {Map<String, String> additionalSettings = const {}}) { |       {Map<String, dynamic> additionalSettings = const {}}) { | ||||||
|     return FDroid().tryInferringAppId(standardUrl); |     return FDroid().tryInferringAppId(standardUrl); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     String? appId = tryInferringAppId(standardUrl); |     String? appId = tryInferringAppId(standardUrl); | ||||||
|     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( |     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ class Mullvad extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await get(Uri.parse('$standardUrl/en/download/android')); |     Response res = await get(Uri.parse('$standardUrl/en/download/android')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -43,7 +43,7 @@ class Mullvad extends AppSource { | |||||||
|           ['https://mullvad.net/download/app/apk/latest'], |           ['https://mullvad.net/download/app/apk/latest'], | ||||||
|           AppNames(name, 'Mullvad-VPN')); |           AppNames(name, 'Mullvad-VPN')); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class Signal extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = |     Response res = | ||||||
|         await get(Uri.parse('https://updates.$host/android/latest.json')); |         await get(Uri.parse('https://updates.$host/android/latest.json')); | ||||||
| @@ -33,7 +33,7 @@ class Signal extends AppSource { | |||||||
|       } |       } | ||||||
|       return APKDetails(version, apkUrls, AppNames(name, 'Signal')); |       return APKDetails(version, apkUrls, AppNames(name, 'Signal')); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ class SourceForge extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await get(Uri.parse('$standardUrl/rss?path=/')); |     Response res = await get(Uri.parse('$standardUrl/rss?path=/')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
| @@ -57,7 +57,7 @@ class SourceForge extends AppSource { | |||||||
|           AppNames( |           AppNames( | ||||||
|               name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1))); |               name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1))); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,10 +10,7 @@ class SteamMobile extends AppSource { | |||||||
|     host = 'store.steampowered.com'; |     host = 'store.steampowered.com'; | ||||||
|     name = tr('steam'); |     name = tr('steam'); | ||||||
|     additionalSourceAppSpecificSettingFormItems = [ |     additionalSourceAppSpecificSettingFormItems = [ | ||||||
|       [ |       [GeneratedFormDropdown('app', apks.entries.toList(), label: tr('app'))] | ||||||
|         GeneratedFormItem('app', |  | ||||||
|             label: tr('app'), required: true, opts: apks.entries.toList()) |  | ||||||
|       ] |  | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -30,11 +27,11 @@ class SteamMobile extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, String> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await get(Uri.parse('https://$host/mobile')); |     Response res = await get(Uri.parse('https://$host/mobile')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       var apkNamePrefix = additionalSettings['app']; |       var apkNamePrefix = additionalSettings['app'] as String?; | ||||||
|       if (apkNamePrefix == null) { |       if (apkNamePrefix == null) { | ||||||
|         throw NoReleasesError(); |         throw NoReleasesError(); | ||||||
|       } |       } | ||||||
| @@ -57,7 +54,7 @@ class SteamMobile extends AppSource { | |||||||
|       var apkUrls = [links[0]]; |       var apkUrls = [links[0]]; | ||||||
|       return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!)); |       return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!)); | ||||||
|     } else { |     } else { | ||||||
|       throw NoReleasesError(); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,39 +1,123 @@ | |||||||
|  | import 'dart:math'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.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 } | abstract class GeneratedFormItem { | ||||||
|  |  | ||||||
| typedef OnValueChanges = void Function( |  | ||||||
|     Map<String, String> values, bool valid, bool isBuilding); |  | ||||||
|  |  | ||||||
| class GeneratedFormItem { |  | ||||||
|   late String key; |   late String key; | ||||||
|   late String label; |   late String label; | ||||||
|   late FormItemType type; |  | ||||||
|   late bool required; |  | ||||||
|   late int max; |  | ||||||
|   late List<String? Function(String? value)> additionalValidators; |  | ||||||
|   late List<Widget> belowWidgets; |   late List<Widget> belowWidgets; | ||||||
|   late String? hint; |   late dynamic defaultValue; | ||||||
|   late List<MapEntry<String, String>>? opts; |   List<dynamic> additionalValidators; | ||||||
|   late String? defaultValue; |   dynamic ensureType(dynamic val); | ||||||
|  |  | ||||||
|   GeneratedFormItem(this.key, |   GeneratedFormItem(this.key, | ||||||
|       {this.label = 'Input', |       {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.required = true, | ||||||
|       this.max = 1, |       this.max = 1, | ||||||
|       this.additionalValidators = const [], |       this.hint}) | ||||||
|       this.belowWidgets = const [], |       : super(key, | ||||||
|       this.hint, |             label: label, | ||||||
|       this.opts, |             belowWidgets: belowWidgets, | ||||||
|       this.defaultValue}) { |             defaultValue: defaultValue, | ||||||
|     if (type != FormItemType.string) { |             additionalValidators: additionalValidators); | ||||||
|       required = false; |  | ||||||
|     } |   @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; | ||||||
|  |   late bool showLabelWhenNotEmpty; | ||||||
|  |   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', | ||||||
|  |       this.showLabelWhenNotEmpty = true}) | ||||||
|  |       : 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 { | class GeneratedForm extends StatefulWidget { | ||||||
|   const GeneratedForm( |   const GeneratedForm( | ||||||
|       {super.key, required this.items, required this.onValueChanges}); |       {super.key, required this.items, required this.onValueChanges}); | ||||||
| @@ -47,30 +131,43 @@ class GeneratedForm extends StatefulWidget { | |||||||
|  |  | ||||||
| class _GeneratedFormState extends State<GeneratedForm> { | class _GeneratedFormState extends State<GeneratedForm> { | ||||||
|   final _formKey = GlobalKey<FormState>(); |   final _formKey = GlobalKey<FormState>(); | ||||||
|   Map<String, String> values = {}; |   Map<String, dynamic> values = {}; | ||||||
|   late List<List<Widget>> formInputs; |   late List<List<Widget>> formInputs; | ||||||
|   List<List<Widget>> rows = []; |   List<List<Widget>> rows = []; | ||||||
|  |  | ||||||
|   // If any value changes, call this to update the parent with value and validity |   // If any value changes, call this to update the parent with value and validity | ||||||
|   void someValueChanged({bool isBuilding = false}) { |   void someValueChanged({bool isBuilding = false}) { | ||||||
|     Map<String, String> returnValues = {}; |     Map<String, dynamic> returnValues = values; | ||||||
|     var valid = true; |     var valid = true; | ||||||
|     for (int r = 0; r < widget.items.length; r++) { |     for (int r = 0; r < widget.items.length; r++) { | ||||||
|       for (int i = 0; i < widget.items[r].length; i++) { |       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) { |         if (formInputs[r][i] is TextFormField) { | ||||||
|           valid = valid && |           var fieldState = | ||||||
|               ((formInputs[r][i].key as GlobalKey<FormFieldState>) |               (formInputs[r][i].key as GlobalKey<FormFieldState>).currentState; | ||||||
|                       .currentState |           if (fieldState != null) { | ||||||
|                       ?.isValid ?? |             valid = valid && fieldState.isValid; | ||||||
|                   false); |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     widget.onValueChanges(returnValues, valid, isBuilding); |     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 |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
| @@ -80,35 +177,37 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|     int j = 0; |     int j = 0; | ||||||
|     for (var row in widget.items) { |     for (var row in widget.items) { | ||||||
|       for (var e in row) { |       for (var e in row) { | ||||||
|         values[e.key] = e.defaultValue ?? e.opts?.first.key ?? ''; |         values[e.key] = e.defaultValue; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Dynamically create form inputs |     // Dynamically create form inputs | ||||||
|     formInputs = widget.items.asMap().entries.map((row) { |     formInputs = widget.items.asMap().entries.map((row) { | ||||||
|       return row.value.asMap().entries.map((e) { |       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>(); |           final formFieldKey = GlobalKey<FormFieldState>(); | ||||||
|           return TextFormField( |           return TextFormField( | ||||||
|             key: formFieldKey, |             key: formFieldKey, | ||||||
|             initialValue: values[e.value.key], |             initialValue: values[formItem.key], | ||||||
|             autovalidateMode: AutovalidateMode.onUserInteraction, |             autovalidateMode: AutovalidateMode.onUserInteraction, | ||||||
|             onChanged: (value) { |             onChanged: (value) { | ||||||
|               setState(() { |               setState(() { | ||||||
|                 values[e.value.key] = value; |                 values[formItem.key] = value; | ||||||
|                 someValueChanged(); |                 someValueChanged(); | ||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
|             decoration: InputDecoration( |             decoration: InputDecoration( | ||||||
|                 helperText: e.value.label + (e.value.required ? ' *' : ''), |                 helperText: formItem.label + (formItem.required ? ' *' : ''), | ||||||
|                 hintText: e.value.hint), |                 hintText: formItem.hint), | ||||||
|             minLines: e.value.max <= 1 ? null : e.value.max, |             minLines: formItem.max <= 1 ? null : formItem.max, | ||||||
|             maxLines: e.value.max <= 1 ? 1 : e.value.max, |             maxLines: formItem.max <= 1 ? 1 : formItem.max, | ||||||
|             validator: (value) { |             validator: (value) { | ||||||
|               if (e.value.required && (value == null || value.trim().isEmpty)) { |               if (formItem.required && | ||||||
|                 return '${e.value.label} ${tr('requiredInBrackets')}'; |                   (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); |                 String? result = validator(value); | ||||||
|                 if (result != null) { |                 if (result != null) { | ||||||
|                   return result; |                   return result; | ||||||
| @@ -117,21 +216,20 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|               return null; |               return null; | ||||||
|             }, |             }, | ||||||
|           ); |           ); | ||||||
|         } else if (e.value.type == FormItemType.string && |         } else if (formItem is GeneratedFormDropdown) { | ||||||
|             e.value.opts != null) { |           if (formItem.opts!.isEmpty) { | ||||||
|           if (e.value.opts!.isEmpty) { |  | ||||||
|             return Text(tr('dropdownNoOptsError')); |             return Text(tr('dropdownNoOptsError')); | ||||||
|           } |           } | ||||||
|           return DropdownButtonFormField( |           return DropdownButtonFormField( | ||||||
|               decoration: InputDecoration(labelText: e.value.label), |               decoration: InputDecoration(labelText: formItem.label), | ||||||
|               value: values[e.value.key], |               value: values[formItem.key], | ||||||
|               items: e.value.opts! |               items: formItem.opts! | ||||||
|                   .map((e) => |                   .map((e2) => | ||||||
|                       DropdownMenuItem(value: e.key, child: Text(e.value))) |                       DropdownMenuItem(value: e2.key, child: Text(e2.value))) | ||||||
|                   .toList(), |                   .toList(), | ||||||
|               onChanged: (value) { |               onChanged: (value) { | ||||||
|                 setState(() { |                 setState(() { | ||||||
|                   values[e.value.key] = value ?? e.value.opts!.first.key; |                   values[formItem.key] = value ?? formItem.opts!.first.key; | ||||||
|                   someValueChanged(); |                   someValueChanged(); | ||||||
|                 }); |                 }); | ||||||
|               }); |               }); | ||||||
| @@ -147,21 +245,201 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     for (var r = 0; r < formInputs.length; r++) { |     for (var r = 0; r < formInputs.length; r++) { | ||||||
|       for (var e = 0; e < formInputs[r].length; e++) { |       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( |           formInputs[r][e] = Row( | ||||||
|             mainAxisAlignment: MainAxisAlignment.spaceBetween, |             mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|             children: [ |             children: [ | ||||||
|               Text(widget.items[r][e].label), |               Text(widget.items[r][e].label), | ||||||
|               Switch( |               Switch( | ||||||
|                   value: values[widget.items[r][e].key] == 'true', |                   value: values[widget.items[r][e].key], | ||||||
|                   onChanged: (value) { |                   onChanged: (value) { | ||||||
|                     setState(() { |                     setState(() { | ||||||
|                       values[widget.items[r][e].key] = value ? 'true' : ''; |                       values[widget.items[r][e].key] = value; | ||||||
|                       someValueChanged(); |                       someValueChanged(); | ||||||
|                     }); |                     }); | ||||||
|                   }) |                   }) | ||||||
|             ], |             ], | ||||||
|           ); |           ); | ||||||
|  |         } else if (widget.items[r][e] is GeneratedFormTagInput) { | ||||||
|  |           formInputs[r][e] = | ||||||
|  |               Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ | ||||||
|  |             if ((values[widget.items[r][e].key] | ||||||
|  |                             as Map<String, MapEntry<int, bool>>?) | ||||||
|  |                         ?.isNotEmpty == | ||||||
|  |                     true && | ||||||
|  |                 (widget.items[r][e] as GeneratedFormTagInput) | ||||||
|  |                     .showLabelWhenNotEmpty) | ||||||
|  |               Column( | ||||||
|  |                 crossAxisAlignment: | ||||||
|  |                     (widget.items[r][e] as GeneratedFormTagInput).alignment == | ||||||
|  |                             WrapAlignment.center | ||||||
|  |                         ? CrossAxisAlignment.center | ||||||
|  |                         : CrossAxisAlignment.stretch, | ||||||
|  |                 children: [ | ||||||
|  |                   Text(widget.items[r][e].label), | ||||||
|  |                   const SizedBox( | ||||||
|  |                     height: 8, | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             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, | ||||||
|  |                       ) | ||||||
|  |                     : 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 ??= {}; | ||||||
|  |                               if (temp[label] == null) { | ||||||
|  |                                 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 +449,8 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|       if (rowInputs.key > 0) { |       if (rowInputs.key > 0) { | ||||||
|         rows.add([ |         rows.add([ | ||||||
|           SizedBox( |           SizedBox( | ||||||
|             height: widget.items[rowInputs.key][0].type == FormItemType.bool && |             height: widget.items[rowInputs.key][0] is GeneratedFormSwitch && | ||||||
|                     widget.items[rowInputs.key - 1][0].type == |                     widget.items[rowInputs.key - 1][0] is! GeneratedFormSwitch | ||||||
|                         FormItemType.string |  | ||||||
|                 ? 25 |                 ? 25 | ||||||
|                 : 8, |                 : 8, | ||||||
|           ) |           ) | ||||||
|   | |||||||
| @@ -9,19 +9,23 @@ class GeneratedFormModal extends StatefulWidget { | |||||||
|       required this.title, |       required this.title, | ||||||
|       required this.items, |       required this.items, | ||||||
|       this.initValid = false, |       this.initValid = false, | ||||||
|       this.message = ''}); |       this.message = '', | ||||||
|  |       this.additionalWidgets = const [], | ||||||
|  |       this.singleNullReturnButton}); | ||||||
|  |  | ||||||
|   final String title; |   final String title; | ||||||
|   final String message; |   final String message; | ||||||
|   final List<List<GeneratedFormItem>> items; |   final List<List<GeneratedFormItem>> items; | ||||||
|   final bool initValid; |   final bool initValid; | ||||||
|  |   final List<Widget> additionalWidgets; | ||||||
|  |   final String? singleNullReturnButton; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<GeneratedFormModal> createState() => _GeneratedFormModalState(); |   State<GeneratedFormModal> createState() => _GeneratedFormModalState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _GeneratedFormModalState extends State<GeneratedFormModal> { | class _GeneratedFormModalState extends State<GeneratedFormModal> { | ||||||
|   Map<String, String> values = {}; |   Map<String, dynamic> values = {}; | ||||||
|   bool valid = false; |   bool valid = false; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -54,24 +58,29 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> { | |||||||
|                   this.valid = valid; |                   this.valid = valid; | ||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
|             }) |             }), | ||||||
|  |         if (widget.additionalWidgets.isNotEmpty) ...widget.additionalWidgets | ||||||
|       ]), |       ]), | ||||||
|       actions: [ |       actions: [ | ||||||
|         TextButton( |         TextButton( | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|               Navigator.of(context).pop(null); |               Navigator.of(context).pop(null); | ||||||
|             }, |             }, | ||||||
|             child: Text(tr('cancel'))), |             child: Text(widget.singleNullReturnButton == null | ||||||
|         TextButton( |                 ? tr('cancel') | ||||||
|             onPressed: !valid |                 : widget.singleNullReturnButton!)), | ||||||
|                 ? null |         widget.singleNullReturnButton == null | ||||||
|                 : () { |             ? TextButton( | ||||||
|                     if (valid) { |                 onPressed: !valid | ||||||
|                       HapticFeedback.selectionClick(); |                     ? null | ||||||
|                       Navigator.of(context).pop(values); |                     : () { | ||||||
|                     } |                         if (valid) { | ||||||
|                   }, |                           HapticFeedback.selectionClick(); | ||||||
|             child: Text(tr('continue'))) |                           Navigator.of(context).pop(values); | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                 child: Text(tr('continue'))) | ||||||
|  |             : const SizedBox.shrink() | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -13,13 +13,10 @@ class ObtainiumError { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class RateLimitError { | class RateLimitError extends ObtainiumError { | ||||||
|   late int remainingMinutes; |   late int remainingMinutes; | ||||||
|   RateLimitError(this.remainingMinutes); |   RateLimitError(this.remainingMinutes) | ||||||
|  |       : super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes)); | ||||||
|   @override |  | ||||||
|   String toString() => |  | ||||||
|       plural('tooManyRequestsTryAgainInMinutes', remainingMinutes); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class InvalidURLError extends ObtainiumError { | class InvalidURLError extends ObtainiumError { | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; | |||||||
| // ignore: implementation_imports | // ignore: implementation_imports | ||||||
| import 'package:easy_localization/src/localization.dart'; | import 'package:easy_localization/src/localization.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.8.23'; | const String currentVersion = '0.9.12'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
| @@ -43,12 +43,16 @@ final globalNavigatorKey = GlobalKey<NavigatorState>(); | |||||||
| Future<void> loadTranslations() async { | Future<void> loadTranslations() async { | ||||||
|   // See easy_localization/issues/210 |   // See easy_localization/issues/210 | ||||||
|   await EasyLocalizationController.initEasyLocation(); |   await EasyLocalizationController.initEasyLocation(); | ||||||
|  |   var s = SettingsProvider(); | ||||||
|  |   await s.initializeSettings(); | ||||||
|  |   var forceLocale = s.forcedLocale; | ||||||
|   final controller = EasyLocalizationController( |   final controller = EasyLocalizationController( | ||||||
|     saveLocale: true, |     saveLocale: true, | ||||||
|  |     forceLocale: forceLocale != null ? Locale(forceLocale) : null, | ||||||
|     fallbackLocale: fallbackLocale, |     fallbackLocale: fallbackLocale, | ||||||
|     supportedLocales: supportedLocales, |     supportedLocales: supportedLocales, | ||||||
|     assetLoader: const RootBundleAssetLoader(), |     assetLoader: const RootBundleAssetLoader(), | ||||||
|     useOnlyLangCode: false, |     useOnlyLangCode: true, | ||||||
|     useFallbackTranslations: true, |     useFallbackTranslations: true, | ||||||
|     path: localeDir, |     path: localeDir, | ||||||
|     onLoadError: (FlutterError e) { |     onLoadError: (FlutterError e) { | ||||||
| @@ -160,6 +164,7 @@ void main() async { | |||||||
|         supportedLocales: supportedLocales, |         supportedLocales: supportedLocales, | ||||||
|         path: localeDir, |         path: localeDir, | ||||||
|         fallbackLocale: fallbackLocale, |         fallbackLocale: fallbackLocale, | ||||||
|  |         useOnlyLangCode: true, | ||||||
|         child: const Obtainium()), |         child: const Obtainium()), | ||||||
|   )); |   )); | ||||||
| } | } | ||||||
| @@ -200,7 +205,7 @@ class _ObtainiumState extends State<Obtainium> { | |||||||
|               currentReleaseTag, |               currentReleaseTag, | ||||||
|               [], |               [], | ||||||
|               0, |               0, | ||||||
|               {'includePrereleases': 'true'}, |               {'includePrereleases': true}, | ||||||
|               null, |               null, | ||||||
|               false) |               false) | ||||||
|         ]); |         ]); | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import 'package:obtainium/custom_errors.dart'; | |||||||
| import 'package:obtainium/main.dart'; | import 'package:obtainium/main.dart'; | ||||||
| import 'package:obtainium/pages/app.dart'; | import 'package:obtainium/pages/app.dart'; | ||||||
| import 'package:obtainium/pages/import_export.dart'; | import 'package:obtainium/pages/import_export.dart'; | ||||||
|  | import 'package:obtainium/pages/settings.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -27,8 +28,9 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|   String userInput = ''; |   String userInput = ''; | ||||||
|   String searchQuery = ''; |   String searchQuery = ''; | ||||||
|   AppSource? pickedSource; |   AppSource? pickedSource; | ||||||
|   Map<String, String> additionalSettings = {}; |   Map<String, dynamic> additionalSettings = {}; | ||||||
|   bool additionalSettingsValid = true; |   bool additionalSettingsValid = true; | ||||||
|  |   List<String> pickedCategories = []; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -37,25 +39,19 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|  |  | ||||||
|     changeUserInput(String input, bool valid, bool isBuilding) { |     changeUserInput(String input, bool valid, bool isBuilding) { | ||||||
|       userInput = input; |       userInput = input; | ||||||
|       fn() { |       if (!isBuilding) { | ||||||
|         var source = valid ? sourceProvider.getSource(userInput) : null; |  | ||||||
|         if (pickedSource.runtimeType != source.runtimeType) { |  | ||||||
|           pickedSource = source; |  | ||||||
|           additionalSettings = source != null |  | ||||||
|               ? getDefaultValuesFromFormItems( |  | ||||||
|                   source.combinedAppSpecificSettingFormItems) |  | ||||||
|               : {}; |  | ||||||
|           additionalSettingsValid = source != null |  | ||||||
|               ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) |  | ||||||
|               : true; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (isBuilding) { |  | ||||||
|         fn(); |  | ||||||
|       } else { |  | ||||||
|         setState(() { |         setState(() { | ||||||
|           fn(); |           var source = valid ? sourceProvider.getSource(userInput) : null; | ||||||
|  |           if (pickedSource.runtimeType != source.runtimeType) { | ||||||
|  |             pickedSource = source; | ||||||
|  |             additionalSettings = source != null | ||||||
|  |                 ? getDefaultValuesFromFormItems( | ||||||
|  |                     source.combinedAppSpecificSettingFormItems) | ||||||
|  |                 : {}; | ||||||
|  |             additionalSettingsValid = source != null | ||||||
|  |                 ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source) | ||||||
|  |                 : true; | ||||||
|  |           } | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -66,9 +62,9 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|       }); |       }); | ||||||
|       var settingsProvider = context.read<SettingsProvider>(); |       var settingsProvider = context.read<SettingsProvider>(); | ||||||
|       () async { |       () async { | ||||||
|         var userPickedTrackOnly = additionalSettings['trackOnly'] == 'true'; |         var userPickedTrackOnly = additionalSettings['trackOnly'] == true; | ||||||
|         var userPickedNoVersionDetection = |         var userPickedNoVersionDetection = | ||||||
|             additionalSettings['noVersionDetection'] == 'true'; |             additionalSettings['noVersionDetection'] == true; | ||||||
|         var cont = true; |         var cont = true; | ||||||
|         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && |         if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && | ||||||
|             await showDialog( |             await showDialog( | ||||||
| @@ -113,7 +109,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|           } |           } | ||||||
|           // Only download the APK here if you need to for the package ID |           // Only download the APK here if you need to for the package ID | ||||||
|           if (sourceProvider.isTempId(app.id) && |           if (sourceProvider.isTempId(app.id) && | ||||||
|               app.additionalSettings['trackOnly'] != 'true') { |               app.additionalSettings['trackOnly'] != true) { | ||||||
|             // ignore: use_build_context_synchronously |             // ignore: use_build_context_synchronously | ||||||
|             var apkUrl = await appsProvider.confirmApkUrl(app, context); |             var apkUrl = await appsProvider.confirmApkUrl(app, context); | ||||||
|             if (apkUrl == null) { |             if (apkUrl == null) { | ||||||
| @@ -128,9 +124,10 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|           if (appsProvider.apps.containsKey(app.id)) { |           if (appsProvider.apps.containsKey(app.id)) { | ||||||
|             throw ObtainiumError(tr('appAlreadyAdded')); |             throw ObtainiumError(tr('appAlreadyAdded')); | ||||||
|           } |           } | ||||||
|           if (app.additionalSettings['trackOnly'] == 'true') { |           if (app.additionalSettings['trackOnly'] == true) { | ||||||
|             app.installedVersion = app.latestVersion; |             app.installedVersion = app.latestVersion; | ||||||
|           } |           } | ||||||
|  |           app.categories = pickedCategories; | ||||||
|           await appsProvider.saveApps([app]); |           await appsProvider.saveApps([app]); | ||||||
|  |  | ||||||
|           return app; |           return app; | ||||||
| @@ -169,7 +166,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                               child: GeneratedForm( |                               child: GeneratedForm( | ||||||
|                                   items: [ |                                   items: [ | ||||||
|                                 [ |                                 [ | ||||||
|                                   GeneratedFormItem('appSourceURL', |                                   GeneratedFormTextField('appSourceURL', | ||||||
|                                       label: tr('appSourceURL'), |                                       label: tr('appSourceURL'), | ||||||
|                                       additionalValidators: [ |                                       additionalValidators: [ | ||||||
|                                         (value) { |                                         (value) { | ||||||
| @@ -231,13 +228,16 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                               child: GeneratedForm( |                               child: GeneratedForm( | ||||||
|                                   items: [ |                                   items: [ | ||||||
|                                     [ |                                     [ | ||||||
|                                       GeneratedFormItem('searchSomeSources', |                                       GeneratedFormTextField( | ||||||
|  |                                           'searchSomeSources', | ||||||
|                                           label: tr('searchSomeSourcesLabel'), |                                           label: tr('searchSomeSourcesLabel'), | ||||||
|                                           required: false), |                                           required: false), | ||||||
|                                     ] |                                     ] | ||||||
|                                   ], |                                   ], | ||||||
|                                   onValueChanges: (values, valid, isBuilding) { |                                   onValueChanges: (values, valid, isBuilding) { | ||||||
|                                     if (values.isNotEmpty && valid) { |                                     if (values.isNotEmpty && | ||||||
|  |                                         valid && | ||||||
|  |                                         !isBuilding) { | ||||||
|                                       setState(() { |                                       setState(() { | ||||||
|                                         searchQuery = |                                         searchQuery = | ||||||
|                                             values['searchSomeSources']!.trim(); |                                             values['searchSomeSources']!.trim(); | ||||||
| @@ -288,7 +288,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                           if (selectedUrls != null && |                                           if (selectedUrls != null && | ||||||
|                                               selectedUrls.isNotEmpty) { |                                               selectedUrls.isNotEmpty) { | ||||||
|                                             changeUserInput( |                                             changeUserInput( | ||||||
|                                                 selectedUrls[0], true, true); |                                                 selectedUrls[0], true, false); | ||||||
|                                             addApp(resetUserInputAfter: true); |                                             addApp(resetUserInputAfter: true); | ||||||
|                                           } |                                           } | ||||||
|                                         }).catchError((e) { |                                         }).catchError((e) { | ||||||
| @@ -298,9 +298,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                 child: Text(tr('search'))) |                                 child: Text(tr('search'))) | ||||||
|                           ], |                           ], | ||||||
|                         ), |                         ), | ||||||
|                       if (pickedSource != null && |                       if (pickedSource != null) | ||||||
|                           (pickedSource! |  | ||||||
|                               .combinedAppSpecificSettingFormItems.isNotEmpty)) |  | ||||||
|                         Column( |                         Column( | ||||||
|                           crossAxisAlignment: CrossAxisAlignment.stretch, |                           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                           children: [ |                           children: [ | ||||||
| @@ -327,6 +325,18 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                     }); |                                     }); | ||||||
|                                   } |                                   } | ||||||
|                                 }), |                                 }), | ||||||
|  |                             Column( | ||||||
|  |                               children: [ | ||||||
|  |                                 const SizedBox( | ||||||
|  |                                   height: 16, | ||||||
|  |                                 ), | ||||||
|  |                                 CategoryEditorSelector( | ||||||
|  |                                     alignment: WrapAlignment.start, | ||||||
|  |                                     onSelected: (categories) { | ||||||
|  |                                       pickedCategories = categories; | ||||||
|  |                                     }), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|                           ], |                           ], | ||||||
|                         ) |                         ) | ||||||
|                       else |                       else | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; | |||||||
| import 'package:obtainium/components/generated_form_modal.dart'; | import 'package:obtainium/components/generated_form_modal.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/main.dart'; | import 'package:obtainium/main.dart'; | ||||||
|  | import 'package:obtainium/pages/settings.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -40,7 +41,107 @@ class _AppPageState extends State<AppPage> { | |||||||
|       prevApp = app; |       prevApp = app; | ||||||
|       getUpdate(app.app.id); |       getUpdate(app.app.id); | ||||||
|     } |     } | ||||||
|     var trackOnly = app?.app.additionalSettings['trackOnly'] == 'true'; |     var trackOnly = app?.app.additionalSettings['trackOnly'] == true; | ||||||
|  |  | ||||||
|  |     var infoColumn = Column( | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |       children: [ | ||||||
|  |         GestureDetector( | ||||||
|  |             onTap: () { | ||||||
|  |               if (app?.app.url != null) { | ||||||
|  |                 launchUrlString(app?.app.url ?? '', | ||||||
|  |                     mode: LaunchMode.externalApplication); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             child: Text( | ||||||
|  |               app?.app.url ?? '', | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |               style: const TextStyle( | ||||||
|  |                   decoration: TextDecoration.underline, | ||||||
|  |                   fontStyle: FontStyle.italic, | ||||||
|  |                   fontSize: 12), | ||||||
|  |             )), | ||||||
|  |         const SizedBox( | ||||||
|  |           height: 32, | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]), | ||||||
|  |           textAlign: TextAlign.center, | ||||||
|  |           style: Theme.of(context).textTheme.bodyLarge, | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           '${tr('installedVersionX', args: [ | ||||||
|  |                 app?.app.installedVersion ?? tr('none') | ||||||
|  |               ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ | ||||||
|  |                   tr('app') | ||||||
|  |                 ])}' : ''}', | ||||||
|  |           textAlign: TextAlign.center, | ||||||
|  |           style: Theme.of(context).textTheme.bodyLarge, | ||||||
|  |         ), | ||||||
|  |         const SizedBox( | ||||||
|  |           height: 32, | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           tr('lastUpdateCheckX', args: [ | ||||||
|  |             app?.app.lastUpdateCheck == null | ||||||
|  |                 ? tr('never') | ||||||
|  |                 : '\n${app?.app.lastUpdateCheck?.toLocal()}' | ||||||
|  |           ]), | ||||||
|  |           textAlign: TextAlign.center, | ||||||
|  |           style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), | ||||||
|  |         ), | ||||||
|  |         const SizedBox( | ||||||
|  |           height: 48, | ||||||
|  |         ), | ||||||
|  |         CategoryEditorSelector( | ||||||
|  |             alignment: WrapAlignment.center, | ||||||
|  |             preselected: | ||||||
|  |                 app?.app.categories != null ? app!.app.categories.toSet() : {}, | ||||||
|  |             onSelected: (categories) { | ||||||
|  |               if (app != null) { | ||||||
|  |                 app.app.categories = categories; | ||||||
|  |                 appsProvider.saveApps([app.app]); | ||||||
|  |               } | ||||||
|  |             }), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     var fullInfoColumn = Column( | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |       children: [ | ||||||
|  |         const SizedBox(height: 150), | ||||||
|  |         app?.installedInfo != null | ||||||
|  |             ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ | ||||||
|  |                 Image.memory( | ||||||
|  |                   app!.installedInfo!.icon!, | ||||||
|  |                   height: 150, | ||||||
|  |                   gaplessPlayback: true, | ||||||
|  |                 ) | ||||||
|  |               ]) | ||||||
|  |             : Container(), | ||||||
|  |         const SizedBox( | ||||||
|  |           height: 25, | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           app?.installedInfo?.name ?? app?.app.name ?? tr('app'), | ||||||
|  |           textAlign: TextAlign.center, | ||||||
|  |           style: Theme.of(context).textTheme.displayLarge, | ||||||
|  |         ), | ||||||
|  |         Text( | ||||||
|  |           tr('byX', args: [app?.app.author ?? tr('unknown')]), | ||||||
|  |           textAlign: TextAlign.center, | ||||||
|  |           style: Theme.of(context).textTheme.headlineMedium, | ||||||
|  |         ), | ||||||
|  |         const SizedBox( | ||||||
|  |           height: 32, | ||||||
|  |         ), | ||||||
|  |         infoColumn, | ||||||
|  |         const SizedBox(height: 150) | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: settingsProvider.showAppWebpage ? AppBar() : null, |       appBar: settingsProvider.showAppWebpage ? AppBar() : null, | ||||||
|       backgroundColor: Theme.of(context).colorScheme.surface, |       backgroundColor: Theme.of(context).colorScheme.surface, | ||||||
| @@ -69,88 +170,8 @@ class _AppPageState extends State<AppPage> { | |||||||
|                   : Container() |                   : Container() | ||||||
|               : CustomScrollView( |               : CustomScrollView( | ||||||
|                   slivers: [ |                   slivers: [ | ||||||
|                     SliverFillRemaining( |                     SliverToBoxAdapter( | ||||||
|                         child: Column( |                         child: Column(children: [fullInfoColumn])), | ||||||
|                       mainAxisAlignment: MainAxisAlignment.center, |  | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|                       children: [ |  | ||||||
|                         app?.installedInfo != null |  | ||||||
|                             ? Row( |  | ||||||
|                                 mainAxisAlignment: MainAxisAlignment.center, |  | ||||||
|                                 children: [ |  | ||||||
|                                     Image.memory( |  | ||||||
|                                       app!.installedInfo!.icon!, |  | ||||||
|                                       height: 150, |  | ||||||
|                                       gaplessPlayback: true, |  | ||||||
|                                     ) |  | ||||||
|                                   ]) |  | ||||||
|                             : Container(), |  | ||||||
|                         const SizedBox( |  | ||||||
|                           height: 25, |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           app?.installedInfo?.name ?? |  | ||||||
|                               app?.app.name ?? |  | ||||||
|                               tr('app'), |  | ||||||
|                           textAlign: TextAlign.center, |  | ||||||
|                           style: Theme.of(context).textTheme.displayLarge, |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           tr('byX', args: [app?.app.author ?? tr('unknown')]), |  | ||||||
|                           textAlign: TextAlign.center, |  | ||||||
|                           style: Theme.of(context).textTheme.headlineMedium, |  | ||||||
|                         ), |  | ||||||
|                         const SizedBox( |  | ||||||
|                           height: 32, |  | ||||||
|                         ), |  | ||||||
|                         GestureDetector( |  | ||||||
|                             onTap: () { |  | ||||||
|                               if (app?.app.url != null) { |  | ||||||
|                                 launchUrlString(app?.app.url ?? '', |  | ||||||
|                                     mode: LaunchMode.externalApplication); |  | ||||||
|                               } |  | ||||||
|                             }, |  | ||||||
|                             child: Text( |  | ||||||
|                               app?.app.url ?? '', |  | ||||||
|                               textAlign: TextAlign.center, |  | ||||||
|                               style: const TextStyle( |  | ||||||
|                                   decoration: TextDecoration.underline, |  | ||||||
|                                   fontStyle: FontStyle.italic, |  | ||||||
|                                   fontSize: 12), |  | ||||||
|                             )), |  | ||||||
|                         const SizedBox( |  | ||||||
|                           height: 32, |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           tr('latestVersionX', |  | ||||||
|                               args: [app?.app.latestVersion ?? tr('unknown')]), |  | ||||||
|                           textAlign: TextAlign.center, |  | ||||||
|                           style: Theme.of(context).textTheme.bodyLarge, |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           '${tr('installedVersionX', args: [ |  | ||||||
|                                 app?.app.installedVersion ?? tr('none') |  | ||||||
|                               ])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [ |  | ||||||
|                                   tr('app') |  | ||||||
|                                 ])}' : ''}', |  | ||||||
|                           textAlign: TextAlign.center, |  | ||||||
|                           style: Theme.of(context).textTheme.bodyLarge, |  | ||||||
|                         ), |  | ||||||
|                         const SizedBox( |  | ||||||
|                           height: 32, |  | ||||||
|                         ), |  | ||||||
|                         Text( |  | ||||||
|                           tr('lastUpdateCheckX', args: [ |  | ||||||
|                             app?.app.lastUpdateCheck == null |  | ||||||
|                                 ? tr('never') |  | ||||||
|                                 : '\n${app?.app.lastUpdateCheck?.toLocal()}' |  | ||||||
|                           ]), |  | ||||||
|                           textAlign: TextAlign.center, |  | ||||||
|                           style: const TextStyle( |  | ||||||
|                               fontStyle: FontStyle.italic, fontSize: 12), |  | ||||||
|                         ) |  | ||||||
|                       ], |  | ||||||
|                     )), |  | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ), | ||||||
|           onRefresh: () async { |           onRefresh: () async { | ||||||
| @@ -227,7 +248,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|                               onPressed: app?.downloadProgress != null |                               onPressed: app?.downloadProgress != null | ||||||
|                                   ? null |                                   ? null | ||||||
|                                   : () { |                                   : () { | ||||||
|                                       showDialog<Map<String, String>>( |                                       showDialog<Map<String, dynamic>?>( | ||||||
|                                           context: context, |                                           context: context, | ||||||
|                                           builder: (BuildContext ctx) { |                                           builder: (BuildContext ctx) { | ||||||
|                                             var items = source |                                             var items = source | ||||||
| @@ -255,7 +276,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                               values; |                                               values; | ||||||
|                                           if (source.enforceTrackOnly) { |                                           if (source.enforceTrackOnly) { | ||||||
|                                             changedApp.additionalSettings[ |                                             changedApp.additionalSettings[ | ||||||
|                                                 'trackOnly'] = 'true'; |                                                 'trackOnly'] = true; | ||||||
|                                             showError( |                                             showError( | ||||||
|                                                 tr('appsFromSourceAreTrackOnly'), |                                                 tr('appsFromSourceAreTrackOnly'), | ||||||
|                                                 context); |                                                 context); | ||||||
| @@ -269,6 +290,31 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                     }, |                                     }, | ||||||
|                               tooltip: tr('additionalOptions'), |                               tooltip: tr('additionalOptions'), | ||||||
|                               icon: const Icon(Icons.settings)), |                               icon: const Icon(Icons.settings)), | ||||||
|  |                         if (app != null && settingsProvider.showAppWebpage) | ||||||
|  |                           IconButton( | ||||||
|  |                               onPressed: () { | ||||||
|  |                                 showDialog( | ||||||
|  |                                     context: context, | ||||||
|  |                                     builder: (BuildContext ctx) { | ||||||
|  |                                       return AlertDialog( | ||||||
|  |                                         scrollable: true, | ||||||
|  |                                         content: infoColumn, | ||||||
|  |                                         title: Text( | ||||||
|  |                                             '${app.app.name} ${tr('byX', args: [ | ||||||
|  |                                               app.app.author | ||||||
|  |                                             ])}'), | ||||||
|  |                                         actions: [ | ||||||
|  |                                           TextButton( | ||||||
|  |                                               onPressed: () { | ||||||
|  |                                                 Navigator.of(context).pop(); | ||||||
|  |                                               }, | ||||||
|  |                                               child: Text(tr('continue'))) | ||||||
|  |                                         ], | ||||||
|  |                                       ); | ||||||
|  |                                     }); | ||||||
|  |                               }, | ||||||
|  |                               icon: const Icon(Icons.more_horiz), | ||||||
|  |                               tooltip: tr('more')), | ||||||
|                         const SizedBox(width: 16.0), |                         const SizedBox(width: 16.0), | ||||||
|                         Expanded( |                         Expanded( | ||||||
|                             child: ElevatedButton( |                             child: ElevatedButton( | ||||||
| @@ -281,7 +327,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|                                         () async { |                                         () async { | ||||||
|                                           if (app?.app.additionalSettings[ |                                           if (app?.app.additionalSettings[ | ||||||
|                                                   'trackOnly'] != |                                                   'trackOnly'] != | ||||||
|                                               'true') { |                                               true) { | ||||||
|                                             await settingsProvider |                                             await settingsProvider | ||||||
|                                                 .getInstallPermission(); |                                                 .getInstallPermission(); | ||||||
|                                           } |                                           } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import 'package:obtainium/components/generated_form_modal.dart'; | |||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/main.dart'; | import 'package:obtainium/main.dart'; | ||||||
| import 'package:obtainium/pages/app.dart'; | import 'package:obtainium/pages/app.dart'; | ||||||
|  | import 'package:obtainium/pages/settings.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -22,7 +23,8 @@ class AppsPage extends StatefulWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class AppsPageState extends State<AppsPage> { | class AppsPageState extends State<AppsPage> { | ||||||
|   AppsFilter? filter; |   AppsFilter filter = AppsFilter(); | ||||||
|  |   final AppsFilter neutralFilter = AppsFilter(); | ||||||
|   var updatesOnlyFilter = |   var updatesOnlyFilter = | ||||||
|       AppsFilter(includeUptodate: false, includeNonInstalled: false); |       AppsFilter(includeUptodate: false, includeNonInstalled: false); | ||||||
|   Set<App> selectedApps = {}; |   Set<App> selectedApps = {}; | ||||||
| @@ -54,7 +56,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     var settingsProvider = context.watch<SettingsProvider>(); |     var settingsProvider = context.watch<SettingsProvider>(); | ||||||
|     var sortedApps = appsProvider.apps.values.toList(); |     var sortedApps = appsProvider.apps.values.toList(); | ||||||
|     var currentFilterIsUpdatesOnly = |     var currentFilterIsUpdatesOnly = | ||||||
|         filter?.isIdenticalTo(updatesOnlyFilter) ?? false; |         filter.isIdenticalTo(updatesOnlyFilter, settingsProvider); | ||||||
|  |  | ||||||
|     selectedApps = selectedApps |     selectedApps = selectedApps | ||||||
|         .where((element) => sortedApps.map((e) => e.app).contains(element)) |         .where((element) => sortedApps.map((e) => e.app).contains(element)) | ||||||
| @@ -70,24 +72,20 @@ class AppsPageState extends State<AppsPage> { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (filter != null) { |     sortedApps = sortedApps.where((app) { | ||||||
|       sortedApps = sortedApps.where((app) { |       if (app.app.installedVersion == app.app.latestVersion && | ||||||
|         if (app.app.installedVersion == app.app.latestVersion && |           !(filter.includeUptodate)) { | ||||||
|             !(filter!.includeUptodate)) { |         return false; | ||||||
|           return false; |       } | ||||||
|         } |       if (app.app.installedVersion == null && !(filter.includeNonInstalled)) { | ||||||
|         if (app.app.installedVersion == null && |         return false; | ||||||
|             !(filter!.includeNonInstalled)) { |       } | ||||||
|           return false; |       if (filter.nameFilter.isNotEmpty || filter.authorFilter.isNotEmpty) { | ||||||
|         } |         List<String> nameTokens = filter.nameFilter | ||||||
|         if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) { |  | ||||||
|           return true; |  | ||||||
|         } |  | ||||||
|         List<String> nameTokens = filter!.nameFilter |  | ||||||
|             .split(' ') |             .split(' ') | ||||||
|             .where((element) => element.trim().isNotEmpty) |             .where((element) => element.trim().isNotEmpty) | ||||||
|             .toList(); |             .toList(); | ||||||
|         List<String> authorTokens = filter!.authorFilter |         List<String> authorTokens = filter.authorFilter | ||||||
|             .split(' ') |             .split(' ') | ||||||
|             .where((element) => element.trim().isNotEmpty) |             .where((element) => element.trim().isNotEmpty) | ||||||
|             .toList(); |             .toList(); | ||||||
| @@ -103,9 +101,15 @@ class AppsPageState extends State<AppsPage> { | |||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         return true; |       } | ||||||
|       }).toList(); |       if (filter.categoryFilter.isNotEmpty && | ||||||
|     } |           filter.categoryFilter | ||||||
|  |               .intersection(app.app.categories.toSet()) | ||||||
|  |               .isEmpty) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       return true; | ||||||
|  |     }).toList(); | ||||||
|  |  | ||||||
|     sortedApps.sort((a, b) { |     sortedApps.sort((a, b) { | ||||||
|       var nameA = a.installedInfo?.name ?? a.app.name; |       var nameA = a.installedInfo?.name ?? a.app.name; | ||||||
| @@ -139,16 +143,14 @@ class AppsPageState extends State<AppsPage> { | |||||||
|  |  | ||||||
|     List<String> trackOnlyUpdateIdsAllOrSelected = []; |     List<String> trackOnlyUpdateIdsAllOrSelected = []; | ||||||
|     existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) { |     existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) { | ||||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == |       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) { | ||||||
|           'true') { |  | ||||||
|         trackOnlyUpdateIdsAllOrSelected.add(id); |         trackOnlyUpdateIdsAllOrSelected.add(id); | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|       return true; |       return true; | ||||||
|     }).toList(); |     }).toList(); | ||||||
|     newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) { |     newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) { | ||||||
|       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == |       if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) { | ||||||
|           'true') { |  | ||||||
|         trackOnlyUpdateIdsAllOrSelected.add(id); |         trackOnlyUpdateIdsAllOrSelected.add(id); | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
| @@ -225,116 +227,152 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               String? changesUrl = SourceProvider() |               String? changesUrl = SourceProvider() | ||||||
|                   .getSource(sortedApps[index].app.url) |                   .getSource(sortedApps[index].app.url) | ||||||
|                   .changeLogPageFromStandardUrl(sortedApps[index].app.url); |                   .changeLogPageFromStandardUrl(sortedApps[index].app.url); | ||||||
|               return ListTile( |               var transparent = const Color.fromARGB(0, 0, 0, 0).value; | ||||||
|                 tileColor: sortedApps[index].app.pinned |               return Container( | ||||||
|                     ? Colors.grey.withOpacity(0.1) |                   decoration: BoxDecoration( | ||||||
|                     : Colors.transparent, |                       border: Border.symmetric( | ||||||
|                 selectedTileColor: Theme.of(context) |                           vertical: BorderSide( | ||||||
|                     .colorScheme |                               width: 4, | ||||||
|                     .primary |                               color: Color( | ||||||
|                     .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1), |                                   sortedApps[index].app.categories.isNotEmpty | ||||||
|                 selected: selectedApps.contains(sortedApps[index].app), |                                       ? settingsProvider.categories[ | ||||||
|                 onLongPress: () { |                                               sortedApps[index] | ||||||
|                   toggleAppSelected(sortedApps[index].app); |                                                   .app | ||||||
|                 }, |                                                   .categories | ||||||
|                 leading: sortedApps[index].installedInfo != null |                                                   .first] ?? | ||||||
|                     ? Image.memory( |                                           transparent | ||||||
|                         sortedApps[index].installedInfo!.icon!, |                                       : transparent)))), | ||||||
|                         gaplessPlayback: true, |                   child: ListTile( | ||||||
|                       ) |                     tileColor: sortedApps[index].app.pinned | ||||||
|                     : null, |                         ? Colors.grey.withOpacity(0.1) | ||||||
|                 title: Text( |                         : Colors.transparent, | ||||||
|                   sortedApps[index].installedInfo?.name ?? |                     selectedTileColor: Theme.of(context) | ||||||
|                       sortedApps[index].app.name, |                         .colorScheme | ||||||
|                   style: TextStyle( |                         .primary | ||||||
|                       fontWeight: sortedApps[index].app.pinned |                         .withOpacity(sortedApps[index].app.pinned ? 0.2 : 0.1), | ||||||
|                           ? FontWeight.bold |                     selected: selectedApps.contains(sortedApps[index].app), | ||||||
|                           : FontWeight.normal), |                     onLongPress: () { | ||||||
|                 ), |                       toggleAppSelected(sortedApps[index].app); | ||||||
|                 subtitle: Text(tr('byX', args: [sortedApps[index].app.author]), |                     }, | ||||||
|                     style: TextStyle( |                     leading: sortedApps[index].installedInfo != null | ||||||
|  |                         ? Image.memory( | ||||||
|  |                             sortedApps[index].installedInfo!.icon!, | ||||||
|  |                             gaplessPlayback: true, | ||||||
|  |                           ) | ||||||
|  |                         : null, | ||||||
|  |                     title: Text( | ||||||
|  |                       sortedApps[index].installedInfo?.name ?? | ||||||
|  |                           sortedApps[index].app.name, | ||||||
|  |                       style: TextStyle( | ||||||
|                         fontWeight: sortedApps[index].app.pinned |                         fontWeight: sortedApps[index].app.pinned | ||||||
|                             ? FontWeight.bold |                             ? FontWeight.bold | ||||||
|                             : FontWeight.normal)), |                             : FontWeight.normal, | ||||||
|                 trailing: SingleChildScrollView( |                       ), | ||||||
|                     reverse: true, |                     ), | ||||||
|                     child: sortedApps[index].downloadProgress != null |                     subtitle: Text( | ||||||
|                         ? Text(tr('percentProgress', args: [ |                         tr('byX', args: [sortedApps[index].app.author]), | ||||||
|                             sortedApps[index] |                         style: TextStyle( | ||||||
|                                     .downloadProgress |                             fontWeight: sortedApps[index].app.pinned | ||||||
|                                     ?.toInt() |                                 ? FontWeight.bold | ||||||
|                                     .toString() ?? |                                 : FontWeight.normal)), | ||||||
|                                 '100' |                     trailing: SingleChildScrollView( | ||||||
|                           ])) |                         reverse: true, | ||||||
|                         : (Column( |                         child: sortedApps[index].downloadProgress != null | ||||||
|                             mainAxisAlignment: MainAxisAlignment.center, |                             ? Text(tr('percentProgress', args: [ | ||||||
|                             crossAxisAlignment: CrossAxisAlignment.end, |                                 sortedApps[index] | ||||||
|                             children: [ |                                         .downloadProgress | ||||||
|                               SizedBox( |                                         ?.toInt() | ||||||
|                                   width: 100, |                                         .toString() ?? | ||||||
|                                   child: Text( |                                     '100' | ||||||
|                                     '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBrackets')}' : ''}', |                               ])) | ||||||
|                                     overflow: TextOverflow.fade, |                             : (Column( | ||||||
|                                     textAlign: TextAlign.end, |                                 mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                                   )), |                                 crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                               sortedApps[index].app.installedVersion != null && |                                 children: [ | ||||||
|                                       sortedApps[index].app.installedVersion != |                                   SizedBox( | ||||||
|                                           sortedApps[index].app.latestVersion |                                       width: 100, | ||||||
|                                   ? GestureDetector( |                                       child: Text( | ||||||
|                                       onTap: changesUrl == null |                                         '${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}', | ||||||
|                                           ? null |                                         overflow: TextOverflow.fade, | ||||||
|                                           : () { |                                         textAlign: TextAlign.end, | ||||||
|                                               launchUrlString(changesUrl, |                                       )), | ||||||
|                                                   mode: LaunchMode |                                   sortedApps[index].app.installedVersion != | ||||||
|                                                       .externalApplication); |                                               null && | ||||||
|                                             }, |                                           sortedApps[index] | ||||||
|                                       child: appsProvider.areDownloadsRunning() |                                                   .app | ||||||
|                                           ? Text(tr('pleaseWait')) |                                                   .installedVersion != | ||||||
|                                           : Text( |                                               sortedApps[index] | ||||||
|                                               '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == 'true' ? ' ${tr('estimateInBracketsShort')}' : ''}', |                                                   .app | ||||||
|                                               style: TextStyle( |                                                   .latestVersion | ||||||
|                                                   fontStyle: FontStyle.italic, |                                       ? GestureDetector( | ||||||
|                                                   decoration: changesUrl == null |                                           onTap: changesUrl == null | ||||||
|                                                       ? TextDecoration.none |                                               ? null | ||||||
|                                                       : TextDecoration |                                               : () { | ||||||
|                                                           .underline), |                                                   launchUrlString(changesUrl, | ||||||
|                                             )) |                                                       mode: LaunchMode | ||||||
|                                   : const SizedBox(), |                                                           .externalApplication); | ||||||
|                             ], |                                                 }, | ||||||
|                           ))), |                                           child: appsProvider | ||||||
|                 onTap: () { |                                                   .areDownloadsRunning() | ||||||
|                   if (selectedApps.isNotEmpty) { |                                               ? Text(tr('pleaseWait')) | ||||||
|                     toggleAppSelected(sortedApps[index].app); |                                               : Text( | ||||||
|                   } else { |                                                   '${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}', | ||||||
|                     Navigator.push( |                                                   style: TextStyle( | ||||||
|                       context, |                                                       fontStyle: | ||||||
|                       MaterialPageRoute( |                                                           FontStyle.italic, | ||||||
|                           builder: (context) => |                                                       decoration: changesUrl == | ||||||
|                               AppPage(appId: sortedApps[index].app.id)), |                                                               null | ||||||
|                     ); |                                                           ? TextDecoration.none | ||||||
|                   } |                                                           : TextDecoration | ||||||
|                 }, |                                                               .underline), | ||||||
|               ); |                                                 )) | ||||||
|  |                                       : const SizedBox(), | ||||||
|  |                                 ], | ||||||
|  |                               ))), | ||||||
|  |                     onTap: () { | ||||||
|  |                       if (selectedApps.isNotEmpty) { | ||||||
|  |                         toggleAppSelected(sortedApps[index].app); | ||||||
|  |                       } else { | ||||||
|  |                         Navigator.push( | ||||||
|  |                           context, | ||||||
|  |                           MaterialPageRoute( | ||||||
|  |                               builder: (context) => | ||||||
|  |                                   AppPage(appId: sortedApps[index].app.id)), | ||||||
|  |                         ); | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                   )); | ||||||
|             }, childCount: sortedApps.length)) |             }, childCount: sortedApps.length)) | ||||||
|           ])), |           ])), | ||||||
|       persistentFooterButtons: [ |       persistentFooterButtons: [ | ||||||
|         Row( |         Row( | ||||||
|           children: [ |           children: [ | ||||||
|             IconButton( |             selectedApps.isEmpty | ||||||
|                 onPressed: () { |                 ? IconButton( | ||||||
|                   selectedApps.isEmpty |                     visualDensity: VisualDensity.compact, | ||||||
|                       ? selectThese(sortedApps.map((e) => e.app).toList()) |                     onPressed: () { | ||||||
|                       : clearSelected(); |                       selectThese(sortedApps.map((e) => e.app).toList()); | ||||||
|                 }, |                     }, | ||||||
|                 icon: Icon( |                     icon: Icon( | ||||||
|                   selectedApps.isEmpty |                       Icons.select_all_outlined, | ||||||
|                       ? Icons.select_all_outlined |                       color: Theme.of(context).colorScheme.primary, | ||||||
|                       : Icons.deselect_outlined, |                     ), | ||||||
|                   color: Theme.of(context).colorScheme.primary, |                     tooltip: tr('selectAll')) | ||||||
|                 ), |                 : TextButton.icon( | ||||||
|                 tooltip: selectedApps.isEmpty |                     style: | ||||||
|                     ? tr('selectAll') |                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||||
|                     : tr('deselectN', args: [selectedApps.length.toString()])), |                     onPressed: () { | ||||||
|  |                       selectedApps.isEmpty | ||||||
|  |                           ? selectThese(sortedApps.map((e) => e.app).toList()) | ||||||
|  |                           : clearSelected(); | ||||||
|  |                     }, | ||||||
|  |                     icon: Icon( | ||||||
|  |                       selectedApps.isEmpty | ||||||
|  |                           ? Icons.select_all_outlined | ||||||
|  |                           : Icons.deselect_outlined, | ||||||
|  |                       color: Theme.of(context).colorScheme.primary, | ||||||
|  |                     ), | ||||||
|  |                     label: Text(selectedApps.length.toString())), | ||||||
|             const VerticalDivider(), |             const VerticalDivider(), | ||||||
|             Expanded( |             Expanded( | ||||||
|                 child: Row( |                 child: Row( | ||||||
| @@ -345,7 +383,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     : IconButton( |                     : IconButton( | ||||||
|                         visualDensity: VisualDensity.compact, |                         visualDensity: VisualDensity.compact, | ||||||
|                         onPressed: () { |                         onPressed: () { | ||||||
|                           showDialog<Map<String, String>?>( |                           showDialog<Map<String, dynamic>?>( | ||||||
|                               context: context, |                               context: context, | ||||||
|                               builder: (BuildContext ctx) { |                               builder: (BuildContext ctx) { | ||||||
|                                 return GeneratedFormModal( |                                 return GeneratedFormModal( | ||||||
| @@ -379,40 +417,33 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                             HapticFeedback.heavyImpact(); |                             HapticFeedback.heavyImpact(); | ||||||
|                             List<GeneratedFormItem> formItems = []; |                             List<GeneratedFormItem> formItems = []; | ||||||
|                             if (existingUpdateIdsAllOrSelected.isNotEmpty) { |                             if (existingUpdateIdsAllOrSelected.isNotEmpty) { | ||||||
|                               formItems.add(GeneratedFormItem('updates', |                               formItems.add(GeneratedFormSwitch('updates', | ||||||
|                                   label: tr('updateX', args: [ |                                   label: tr('updateX', args: [ | ||||||
|                                     plural('apps', |                                     plural('apps', | ||||||
|                                         existingUpdateIdsAllOrSelected.length) |                                         existingUpdateIdsAllOrSelected.length) | ||||||
|                                   ]), |                                   ]), | ||||||
|                                   type: FormItemType.bool, |                                   defaultValue: true)); | ||||||
|                                   defaultValue: 'true')); |  | ||||||
|                             } |                             } | ||||||
|                             if (newInstallIdsAllOrSelected.isNotEmpty) { |                             if (newInstallIdsAllOrSelected.isNotEmpty) { | ||||||
|                               formItems.add(GeneratedFormItem('installs', |                               formItems.add(GeneratedFormSwitch('installs', | ||||||
|                                   label: tr('installX', args: [ |                                   label: tr('installX', args: [ | ||||||
|                                     plural('apps', |                                     plural('apps', | ||||||
|                                         newInstallIdsAllOrSelected.length) |                                         newInstallIdsAllOrSelected.length) | ||||||
|                                   ]), |                                   ]), | ||||||
|                                   type: FormItemType.bool, |                                   defaultValue: existingUpdateIdsAllOrSelected | ||||||
|                                   defaultValue: |                                       .isNotEmpty)); | ||||||
|                                       existingUpdateIdsAllOrSelected.isNotEmpty |  | ||||||
|                                           ? 'true' |  | ||||||
|                                           : '')); |  | ||||||
|                             } |                             } | ||||||
|                             if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { |                             if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { | ||||||
|                               formItems.add(GeneratedFormItem('trackonlies', |                               formItems.add(GeneratedFormSwitch('trackonlies', | ||||||
|                                   label: tr('markXTrackOnlyAsUpdated', args: [ |                                   label: tr('markXTrackOnlyAsUpdated', args: [ | ||||||
|                                     plural('apps', |                                     plural('apps', | ||||||
|                                         trackOnlyUpdateIdsAllOrSelected.length) |                                         trackOnlyUpdateIdsAllOrSelected.length) | ||||||
|                                   ]), |                                   ]), | ||||||
|                                   type: FormItemType.bool, |  | ||||||
|                                   defaultValue: existingUpdateIdsAllOrSelected |                                   defaultValue: existingUpdateIdsAllOrSelected | ||||||
|                                               .isNotEmpty || |                                           .isNotEmpty || | ||||||
|                                           newInstallIdsAllOrSelected.isNotEmpty |                                       newInstallIdsAllOrSelected.isNotEmpty)); | ||||||
|                                       ? 'true' |  | ||||||
|                                       : '')); |  | ||||||
|                             } |                             } | ||||||
|                             showDialog<Map<String, String>?>( |                             showDialog<Map<String, dynamic>?>( | ||||||
|                                 context: context, |                                 context: context, | ||||||
|                                 builder: (BuildContext ctx) { |                                 builder: (BuildContext ctx) { | ||||||
|                                   var totalApps = existingUpdateIdsAllOrSelected |                                   var totalApps = existingUpdateIdsAllOrSelected | ||||||
| @@ -432,11 +463,11 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                                       [formItems]); |                                       [formItems]); | ||||||
|                                 } |                                 } | ||||||
|                                 bool shouldInstallUpdates = |                                 bool shouldInstallUpdates = | ||||||
|                                     values['updates'] == 'true'; |                                     values['updates'] == true; | ||||||
|                                 bool shouldInstallNew = |                                 bool shouldInstallNew = | ||||||
|                                     values['installs'] == 'true'; |                                     values['installs'] == true; | ||||||
|                                 bool shouldMarkTrackOnlies = |                                 bool shouldMarkTrackOnlies = | ||||||
|                                     values['trackonlies'] == 'true'; |                                     values['trackonlies'] == true; | ||||||
|                                 (() async { |                                 (() async { | ||||||
|                                   if (shouldInstallNew || |                                   if (shouldInstallNew || | ||||||
|                                       shouldInstallUpdates) { |                                       shouldInstallUpdates) { | ||||||
| @@ -474,6 +505,75 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     icon: const Icon( |                     icon: const Icon( | ||||||
|                       Icons.file_download_outlined, |                       Icons.file_download_outlined, | ||||||
|                     )), |                     )), | ||||||
|  |                 selectedApps.isEmpty | ||||||
|  |                     ? const SizedBox() | ||||||
|  |                     : IconButton( | ||||||
|  |                         visualDensity: VisualDensity.compact, | ||||||
|  |                         onPressed: () async { | ||||||
|  |                           try { | ||||||
|  |                             Set<String>? preselected; | ||||||
|  |                             var showPrompt = false; | ||||||
|  |                             for (var element in selectedApps) { | ||||||
|  |                               var currentCats = element.categories.toSet(); | ||||||
|  |                               if (preselected == null) { | ||||||
|  |                                 preselected = currentCats; | ||||||
|  |                               } else { | ||||||
|  |                                 if (!settingsProvider.setEqual( | ||||||
|  |                                     currentCats, preselected)) { | ||||||
|  |                                   showPrompt = true; | ||||||
|  |                                   break; | ||||||
|  |                                 } | ||||||
|  |                               } | ||||||
|  |                             } | ||||||
|  |                             var cont = true; | ||||||
|  |                             if (showPrompt) { | ||||||
|  |                               cont = await showDialog<Map<String, dynamic>?>( | ||||||
|  |                                       context: context, | ||||||
|  |                                       builder: (BuildContext ctx) { | ||||||
|  |                                         return GeneratedFormModal( | ||||||
|  |                                           title: tr('categorize'), | ||||||
|  |                                           items: const [], | ||||||
|  |                                           initValid: true, | ||||||
|  |                                           message: | ||||||
|  |                                               tr('selectedCategorizeWarning'), | ||||||
|  |                                         ); | ||||||
|  |                                       }) != | ||||||
|  |                                   null; | ||||||
|  |                             } | ||||||
|  |                             if (cont) { | ||||||
|  |                               await showDialog<Map<String, dynamic>?>( | ||||||
|  |                                   context: context, | ||||||
|  |                                   builder: (BuildContext ctx) { | ||||||
|  |                                     return GeneratedFormModal( | ||||||
|  |                                       title: tr('categorize'), | ||||||
|  |                                       items: const [], | ||||||
|  |                                       initValid: true, | ||||||
|  |                                       singleNullReturnButton: tr('continue'), | ||||||
|  |                                       additionalWidgets: [ | ||||||
|  |                                         CategoryEditorSelector( | ||||||
|  |                                           preselected: !showPrompt | ||||||
|  |                                               ? preselected ?? {} | ||||||
|  |                                               : {}, | ||||||
|  |                                           showLabelWhenNotEmpty: false, | ||||||
|  |                                           onSelected: (categories) { | ||||||
|  |                                             appsProvider | ||||||
|  |                                                 .saveApps(selectedApps.map((e) { | ||||||
|  |                                               e.categories = categories; | ||||||
|  |                                               return e; | ||||||
|  |                                             }).toList()); | ||||||
|  |                                           }, | ||||||
|  |                                         ) | ||||||
|  |                                       ], | ||||||
|  |                                     ); | ||||||
|  |                                   }); | ||||||
|  |                             } | ||||||
|  |                           } catch (err) { | ||||||
|  |                             showError(err, context); | ||||||
|  |                           } | ||||||
|  |                         }, | ||||||
|  |                         tooltip: tr('categorize'), | ||||||
|  |                         icon: const Icon(Icons.category_outlined), | ||||||
|  |                       ), | ||||||
|                 selectedApps.isEmpty |                 selectedApps.isEmpty | ||||||
|                     ? const SizedBox() |                     ? const SizedBox() | ||||||
|                     : IconButton( |                     : IconButton( | ||||||
| @@ -651,7 +751,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 setState(() { |                 setState(() { | ||||||
|                   if (currentFilterIsUpdatesOnly) { |                   if (currentFilterIsUpdatesOnly) { | ||||||
|                     filter = null; |                     filter = AppsFilter(); | ||||||
|                   } else { |                   } else { | ||||||
|                     filter = updatesOnlyFilter; |                     filter = updatesOnlyFilter; | ||||||
|                   } |                   } | ||||||
| @@ -670,53 +770,64 @@ class AppsPageState extends State<AppsPage> { | |||||||
|             appsProvider.apps.isEmpty |             appsProvider.apps.isEmpty | ||||||
|                 ? const SizedBox() |                 ? const SizedBox() | ||||||
|                 : TextButton.icon( |                 : TextButton.icon( | ||||||
|  |                     style: | ||||||
|  |                         const ButtonStyle(visualDensity: VisualDensity.compact), | ||||||
|                     label: Text( |                     label: Text( | ||||||
|                       filter == null ? tr('filter') : tr('filterActive'), |                       filter.isIdenticalTo(neutralFilter, settingsProvider) | ||||||
|  |                           ? tr('filter') | ||||||
|  |                           : tr('filterActive'), | ||||||
|                       style: TextStyle( |                       style: TextStyle( | ||||||
|                           fontWeight: filter == null |                           fontWeight: filter.isIdenticalTo( | ||||||
|  |                                   neutralFilter, settingsProvider) | ||||||
|                               ? FontWeight.normal |                               ? FontWeight.normal | ||||||
|                               : FontWeight.bold), |                               : FontWeight.bold), | ||||||
|                     ), |                     ), | ||||||
|                     onPressed: () { |                     onPressed: () { | ||||||
|                       showDialog<Map<String, String>?>( |                       showDialog<Map<String, dynamic>?>( | ||||||
|                           context: context, |                           context: context, | ||||||
|                           builder: (BuildContext ctx) { |                           builder: (BuildContext ctx) { | ||||||
|                             var vals = filter == null |                             var vals = filter.toFormValuesMap(); | ||||||
|                                 ? AppsFilter().toValuesMap() |  | ||||||
|                                 : filter!.toValuesMap(); |  | ||||||
|                             return GeneratedFormModal( |                             return GeneratedFormModal( | ||||||
|                                 title: tr('filterApps'), |                               initValid: true, | ||||||
|                                 items: [ |                               title: tr('filterApps'), | ||||||
|                                   [ |                               items: [ | ||||||
|                                     GeneratedFormItem('appName', |                                 [ | ||||||
|                                         label: tr('appName'), |                                   GeneratedFormTextField('appName', | ||||||
|                                         required: false, |                                       label: tr('appName'), | ||||||
|                                         defaultValue: vals['appName']), |                                       required: false, | ||||||
|                                     GeneratedFormItem('author', |                                       defaultValue: vals['appName']), | ||||||
|                                         label: tr('author'), |                                   GeneratedFormTextField('author', | ||||||
|                                         required: false, |                                       label: tr('author'), | ||||||
|                                         defaultValue: vals['author']) |                                       required: false, | ||||||
|                                   ], |                                       defaultValue: vals['author']) | ||||||
|                                   [ |                                 ], | ||||||
|                                     GeneratedFormItem('upToDateApps', |                                 [ | ||||||
|                                         label: tr('upToDateApps'), |                                   GeneratedFormSwitch('upToDateApps', | ||||||
|                                         type: FormItemType.bool, |                                       label: tr('upToDateApps'), | ||||||
|                                         defaultValue: vals['upToDateApps']) |                                       defaultValue: vals['upToDateApps']) | ||||||
|                                   ], |                                 ], | ||||||
|                                   [ |                                 [ | ||||||
|                                     GeneratedFormItem('nonInstalledApps', |                                   GeneratedFormSwitch('nonInstalledApps', | ||||||
|                                         label: tr('nonInstalledApps'), |                                       label: tr('nonInstalledApps'), | ||||||
|                                         type: FormItemType.bool, |                                       defaultValue: vals['nonInstalledApps']) | ||||||
|                                         defaultValue: vals['nonInstalledApps']) |                                 ] | ||||||
|                                   ] |                               ], | ||||||
|                                 ]); |                               additionalWidgets: [ | ||||||
|  |                                 const SizedBox( | ||||||
|  |                                   height: 16, | ||||||
|  |                                 ), | ||||||
|  |                                 CategoryEditorSelector( | ||||||
|  |                                   preselected: filter.categoryFilter, | ||||||
|  |                                   onSelected: (categories) { | ||||||
|  |                                     filter.categoryFilter = categories.toSet(); | ||||||
|  |                                   }, | ||||||
|  |                                 ) | ||||||
|  |                               ], | ||||||
|  |                             ); | ||||||
|                           }).then((values) { |                           }).then((values) { | ||||||
|                         if (values != null) { |                         if (values != null) { | ||||||
|                           setState(() { |                           setState(() { | ||||||
|                             filter = AppsFilter.fromValuesMap(values); |                             filter.setFormValuesFromMap(values); | ||||||
|                             if (AppsFilter().isIdenticalTo(filter!)) { |  | ||||||
|                               filter = null; |  | ||||||
|                             } |  | ||||||
|                           }); |                           }); | ||||||
|                         } |                         } | ||||||
|                       }); |                       }); | ||||||
| @@ -734,32 +845,35 @@ class AppsFilter { | |||||||
|   late String authorFilter; |   late String authorFilter; | ||||||
|   late bool includeUptodate; |   late bool includeUptodate; | ||||||
|   late bool includeNonInstalled; |   late bool includeNonInstalled; | ||||||
|  |   late Set<String> categoryFilter; | ||||||
|  |  | ||||||
|   AppsFilter( |   AppsFilter( | ||||||
|       {this.nameFilter = '', |       {this.nameFilter = '', | ||||||
|       this.authorFilter = '', |       this.authorFilter = '', | ||||||
|       this.includeUptodate = true, |       this.includeUptodate = true, | ||||||
|       this.includeNonInstalled = true}); |       this.includeNonInstalled = true, | ||||||
|  |       this.categoryFilter = const {}}); | ||||||
|  |  | ||||||
|   Map<String, String> toValuesMap() { |   Map<String, dynamic> toFormValuesMap() { | ||||||
|     return { |     return { | ||||||
|       'appName': nameFilter, |       'appName': nameFilter, | ||||||
|       'author': authorFilter, |       'author': authorFilter, | ||||||
|       'upToDateApps': includeUptodate ? 'true' : '', |       'upToDateApps': includeUptodate, | ||||||
|       'nonInstalledApps': includeNonInstalled ? 'true' : '' |       'nonInstalledApps': includeNonInstalled | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   AppsFilter.fromValuesMap(Map<String, String> values) { |   setFormValuesFromMap(Map<String, dynamic> values) { | ||||||
|     nameFilter = values['appName']!; |     nameFilter = values['appName']!; | ||||||
|     authorFilter = values['author']!; |     authorFilter = values['author']!; | ||||||
|     includeUptodate = values['upToDateApps'] == 'true'; |     includeUptodate = values['upToDateApps']; | ||||||
|     includeNonInstalled = values['nonInstalledApps'] == 'true'; |     includeNonInstalled = values['nonInstalledApps']; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool isIdenticalTo(AppsFilter other) => |   bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) => | ||||||
|       authorFilter.trim() == other.authorFilter.trim() && |       authorFilter.trim() == other.authorFilter.trim() && | ||||||
|       nameFilter.trim() == other.nameFilter.trim() && |       nameFilter.trim() == other.nameFilter.trim() && | ||||||
|       includeUptodate == other.includeUptodate && |       includeUptodate == other.includeUptodate && | ||||||
|       includeNonInstalled == other.includeNonInstalled; |       includeNonInstalled == other.includeNonInstalled && | ||||||
|  |       settingsProvider.setEqual(categoryFilter, other.categoryFilter); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -66,6 +66,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                             showError( |                                             showError( | ||||||
|                                                 tr('exportedTo', args: [path]), |                                                 tr('exportedTo', args: [path]), | ||||||
|                                                 context); |                                                 context); | ||||||
|  |                                           }).catchError((e) { | ||||||
|  |                                             showError(e, context); | ||||||
|                                           }); |                                           }); | ||||||
|                                         }, |                                         }, | ||||||
|                                   child: Text(tr('obtainiumExport')))), |                                   child: Text(tr('obtainiumExport')))), | ||||||
| @@ -138,18 +140,19 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                           onPressed: importInProgress |                           onPressed: importInProgress | ||||||
|                               ? null |                               ? null | ||||||
|                               : () { |                               : () { | ||||||
|                                   showDialog( |                                   showDialog<Map<String, dynamic>?>( | ||||||
|                                       context: context, |                                       context: context, | ||||||
|                                       builder: (BuildContext ctx) { |                                       builder: (BuildContext ctx) { | ||||||
|                                         return GeneratedFormModal( |                                         return GeneratedFormModal( | ||||||
|                                           title: tr('importFromURLList'), |                                           title: tr('importFromURLList'), | ||||||
|                                           items: [ |                                           items: [ | ||||||
|                                             [ |                                             [ | ||||||
|                                               GeneratedFormItem('appURLList', |                                               GeneratedFormTextField( | ||||||
|  |                                                   'appURLList', | ||||||
|                                                   label: tr('appURLList'), |                                                   label: tr('appURLList'), | ||||||
|                                                   max: 7, |                                                   max: 7, | ||||||
|                                                   additionalValidators: [ |                                                   additionalValidators: [ | ||||||
|                                                     (String? value) { |                                                     (dynamic value) { | ||||||
|                                                       if (value != null && |                                                       if (value != null && | ||||||
|                                                           value.isNotEmpty) { |                                                           value.isNotEmpty) { | ||||||
|                                                         var lines = value |                                                         var lines = value | ||||||
| @@ -176,7 +179,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                       }).then((values) { |                                       }).then((values) { | ||||||
|                                     if (values != null) { |                                     if (values != null) { | ||||||
|                                       var urls = |                                       var urls = | ||||||
|                                           (values[0] as String).split('\n'); |                                           (values['appURLList'] as String) | ||||||
|  |                                               .split('\n'); | ||||||
|                                       setState(() { |                                       setState(() { | ||||||
|                                         importInProgress = true; |                                         importInProgress = true; | ||||||
|                                       }); |                                       }); | ||||||
| @@ -224,7 +228,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                             : () { |                                             : () { | ||||||
|                                                 () async { |                                                 () async { | ||||||
|                                                   var values = await showDialog< |                                                   var values = await showDialog< | ||||||
|                                                           List<String>>( |                                                           Map<String, | ||||||
|  |                                                               dynamic>?>( | ||||||
|                                                       context: context, |                                                       context: context, | ||||||
|                                                       builder: |                                                       builder: | ||||||
|                                                           (BuildContext ctx) { |                                                           (BuildContext ctx) { | ||||||
| @@ -235,7 +240,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                                               ]), |                                                               ]), | ||||||
|                                                           items: [ |                                                           items: [ | ||||||
|                                                             [ |                                                             [ | ||||||
|                                                               GeneratedFormItem( |                                                               GeneratedFormTextField( | ||||||
|                                                                   'searchQuery', |                                                                   'searchQuery', | ||||||
|                                                                   label: tr( |                                                                   label: tr( | ||||||
|                                                                       'searchQuery')) |                                                                       'searchQuery')) | ||||||
| @@ -244,13 +249,17 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                                         ); |                                                         ); | ||||||
|                                                       }); |                                                       }); | ||||||
|                                                   if (values != null && |                                                   if (values != null && | ||||||
|                                                       values[0].isNotEmpty) { |                                                       (values['searchQuery'] | ||||||
|  |                                                                   as String?) | ||||||
|  |                                                               ?.isNotEmpty == | ||||||
|  |                                                           true) { | ||||||
|                                                     setState(() { |                                                     setState(() { | ||||||
|                                                       importInProgress = true; |                                                       importInProgress = true; | ||||||
|                                                     }); |                                                     }); | ||||||
|                                                     var urlsWithDescriptions = |                                                     var urlsWithDescriptions = | ||||||
|                                                         await source |                                                         await source.search( | ||||||
|                                                             .search(values[0]); |                                                             values['searchQuery'] | ||||||
|  |                                                                 as String); | ||||||
|                                                     if (urlsWithDescriptions |                                                     if (urlsWithDescriptions | ||||||
|                                                         .isNotEmpty) { |                                                         .isNotEmpty) { | ||||||
|                                                       var selectedUrls = |                                                       var selectedUrls = | ||||||
| @@ -331,7 +340,9 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                             ? null |                                             ? null | ||||||
|                                             : () { |                                             : () { | ||||||
|                                                 () async { |                                                 () async { | ||||||
|                                                   var values = await showDialog( |                                                   var values = await showDialog< | ||||||
|  |                                                           Map<String, | ||||||
|  |                                                               dynamic>?>( | ||||||
|                                                       context: context, |                                                       context: context, | ||||||
|                                                       builder: |                                                       builder: | ||||||
|                                                           (BuildContext ctx) { |                                                           (BuildContext ctx) { | ||||||
| @@ -345,7 +356,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                                                   .requiredArgs |                                                                   .requiredArgs | ||||||
|                                                                   .map( |                                                                   .map( | ||||||
|                                                                       (e) => [ |                                                                       (e) => [ | ||||||
|                                                                             GeneratedFormItem(e, |                                                                             GeneratedFormTextField(e, | ||||||
|                                                                                 label: e) |                                                                                 label: e) | ||||||
|                                                                           ]) |                                                                           ]) | ||||||
|                                                                   .toList(), |                                                                   .toList(), | ||||||
| @@ -358,7 +369,10 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                                                     var urlsWithDescriptions = |                                                     var urlsWithDescriptions = | ||||||
|                                                         await source |                                                         await source | ||||||
|                                                             .getUrlsWithDescriptions( |                                                             .getUrlsWithDescriptions( | ||||||
|                                                                 values); |                                                                 values.values | ||||||
|  |                                                                     .map((e) => | ||||||
|  |                                                                         e.toString()) | ||||||
|  |                                                                     .toList()); | ||||||
|                                                     var selectedUrls = |                                                     var selectedUrls = | ||||||
|                                                         await showDialog< |                                                         await showDialog< | ||||||
|                                                                 List<String>?>( |                                                                 List<String>?>( | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
|  | import 'dart:math'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:obtainium/components/custom_app_bar.dart'; | import 'package:obtainium/components/custom_app_bar.dart'; | ||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
|  | import 'package:obtainium/components/generated_form_modal.dart'; | ||||||
| import 'package:obtainium/custom_errors.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/logs_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| @@ -17,6 +22,21 @@ class SettingsPage extends StatefulWidget { | |||||||
|   State<SettingsPage> createState() => _SettingsPageState(); |   State<SettingsPage> createState() => _SettingsPageState(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Generates a random light color | ||||||
|  | // Courtesy of ChatGPT 😭 (with a bugfix 🥳) | ||||||
|  | Color generateRandomLightColor() { | ||||||
|  |   // Create a random number generator | ||||||
|  |   final Random random = Random(); | ||||||
|  |  | ||||||
|  |   // Generate random hue, saturation, and value values | ||||||
|  |   final double hue = random.nextDouble() * 360; | ||||||
|  |   final double saturation = 0.5 + random.nextDouble() * 0.5; | ||||||
|  |   final double value = 0.9 + random.nextDouble() * 0.1; | ||||||
|  |  | ||||||
|  |   // Create a HSV color with the random values | ||||||
|  |   return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor(); | ||||||
|  | } | ||||||
|  |  | ||||||
| class _SettingsPageState extends State<SettingsPage> { | class _SettingsPageState extends State<SettingsPage> { | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -110,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( |     var intervalDropdown = DropdownButtonFormField( | ||||||
|         decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')), |         decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')), | ||||||
|         value: settingsProvider.updateInterval, |         value: settingsProvider.updateInterval, | ||||||
| @@ -138,11 +180,12 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|     var sourceSpecificFields = sourceProvider.sources.map((e) { |     var sourceSpecificFields = sourceProvider.sources.map((e) { | ||||||
|       if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) { |       if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) { | ||||||
|         return GeneratedForm( |         return GeneratedForm( | ||||||
|             items: e.additionalSourceSpecificSettingFormItems |             items: e.additionalSourceSpecificSettingFormItems.map((e) { | ||||||
|                 .map((e) => [e]) |               e.defaultValue = settingsProvider.getSettingString(e.key); | ||||||
|                 .toList(), |               return [e]; | ||||||
|  |             }).toList(), | ||||||
|             onValueChanges: (values, valid, isBuilding) { |             onValueChanges: (values, valid, isBuilding) { | ||||||
|               if (valid) { |               if (valid && !isBuilding) { | ||||||
|                 values.forEach((key, value) { |                 values.forEach((key, value) { | ||||||
|                   settingsProvider.setSettingString(key, value); |                   settingsProvider.setSettingString(key, value); | ||||||
|                 }); |                 }); | ||||||
| @@ -190,6 +233,8 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|                             height16, |                             height16, | ||||||
|  |                             localeDropdown, | ||||||
|  |                             height16, | ||||||
|                             Row( |                             Row( | ||||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, |                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|                               children: [ |                               children: [ | ||||||
| @@ -232,6 +277,18 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                                   color: Theme.of(context).colorScheme.primary), |                                   color: Theme.of(context).colorScheme.primary), | ||||||
|                             ), |                             ), | ||||||
|                             ...sourceSpecificFields, |                             ...sourceSpecificFields, | ||||||
|  |                             const Divider( | ||||||
|  |                               height: 48, | ||||||
|  |                             ), | ||||||
|  |                             Text( | ||||||
|  |                               tr('categories'), | ||||||
|  |                               style: TextStyle( | ||||||
|  |                                   color: Theme.of(context).colorScheme.primary), | ||||||
|  |                             ), | ||||||
|  |                             height16, | ||||||
|  |                             const CategoryEditorSelector( | ||||||
|  |                               showLabelWhenNotEmpty: false, | ||||||
|  |                             ) | ||||||
|                           ], |                           ], | ||||||
|                         ))), |                         ))), | ||||||
|           SliverToBoxAdapter( |           SliverToBoxAdapter( | ||||||
| @@ -346,3 +403,62 @@ 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; | ||||||
|  |   final bool showLabelWhenNotEmpty; | ||||||
|  |   const CategoryEditorSelector( | ||||||
|  |       {super.key, | ||||||
|  |       this.onSelected, | ||||||
|  |       this.singleSelect = false, | ||||||
|  |       this.preselected = const {}, | ||||||
|  |       this.alignment = WrapAlignment.start, | ||||||
|  |       this.showLabelWhenNotEmpty = true}); | ||||||
|  |  | ||||||
|  |   @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('categories'), | ||||||
|  |                 emptyMessage: tr('noCategories'), | ||||||
|  |                 defaultValue: storedValues, | ||||||
|  |                 alignment: widget.alignment, | ||||||
|  |                 deleteConfirmationMessage: MapEntry( | ||||||
|  |                     tr('deleteCategoriesQuestion'), | ||||||
|  |                     tr('categoryDeleteWarning')), | ||||||
|  |                 singleSelect: widget.singleSelect, | ||||||
|  |                 showLabelWhenNotEmpty: widget.showLabelWhenNotEmpty) | ||||||
|  |           ] | ||||||
|  |         ], | ||||||
|  |         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()); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         })); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import 'package:obtainium/providers/logs_provider.dart'; | |||||||
| import 'package:obtainium/providers/notifications_provider.dart'; | import 'package:obtainium/providers/notifications_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:package_archive_info/package_archive_info.dart'; | import 'package:package_archive_info/package_archive_info.dart'; | ||||||
|  | import 'package:permission_handler/permission_handler.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:flutter_fgbg/flutter_fgbg.dart'; | import 'package:flutter_fgbg/flutter_fgbg.dart'; | ||||||
| @@ -313,7 +314,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|         throw ObtainiumError(tr('appNotFound')); |         throw ObtainiumError(tr('appNotFound')); | ||||||
|       } |       } | ||||||
|       String? apkUrl; |       String? apkUrl; | ||||||
|       var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == 'true'; |       var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true; | ||||||
|       if (!trackOnly) { |       if (!trackOnly) { | ||||||
|         apkUrl = await confirmApkUrl(apps[id]!.app, context); |         apkUrl = await confirmApkUrl(apps[id]!.app, context); | ||||||
|       } |       } | ||||||
| @@ -452,9 +453,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|   // Don't save changes, just return the object if changes were made (else null) |   // Don't save changes, just return the object if changes were made (else null) | ||||||
|   App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { |   App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { | ||||||
|     var modded = false; |     var modded = false; | ||||||
|     var trackOnly = app.additionalSettings['trackOnly'] == 'true'; |     var trackOnly = app.additionalSettings['trackOnly'] == true; | ||||||
|     var noVersionDetection = |     var noVersionDetection = | ||||||
|         app.additionalSettings['noVersionDetection'] == 'true'; |         app.additionalSettings['noVersionDetection'] == true; | ||||||
|     if (installedInfo == null && app.installedVersion != null && !trackOnly) { |     if (installedInfo == null && app.installedVersion != null && !trackOnly) { | ||||||
|       app.installedVersion = null; |       app.installedVersion = null; | ||||||
|       modded = true; |       modded = true; | ||||||
| @@ -706,6 +707,14 @@ class AppsProvider with ChangeNotifier { | |||||||
|       exportDir = await getExternalStorageDirectory(); |       exportDir = await getExternalStorageDirectory(); | ||||||
|       path = exportDir!.path; |       path = exportDir!.path; | ||||||
|     } |     } | ||||||
|  |     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) { | ||||||
|  |       if (await Permission.storage.isDenied) { | ||||||
|  |         await Permission.storage.request(); | ||||||
|  |       } | ||||||
|  |       if (await Permission.storage.isDenied) { | ||||||
|  |         throw ObtainiumError(tr('storagePermissionDenied')); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     File export = File( |     File export = File( | ||||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); |         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); | ||||||
|     export.writeAsStringSync( |     export.writeAsStringSync( | ||||||
|   | |||||||
| @@ -1,9 +1,13 @@ | |||||||
| // Exposes functions used to save/load app settings | // Exposes functions used to save/load app settings | ||||||
|  |  | ||||||
|  | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:fluttertoast/fluttertoast.dart'; | import 'package:fluttertoast/fluttertoast.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
|  | import 'package:obtainium/components/generated_form.dart'; | ||||||
|  | import 'package:obtainium/main.dart'; | ||||||
| import 'package:permission_handler/permission_handler.dart'; | import 'package:permission_handler/permission_handler.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  |  | ||||||
| @@ -144,4 +148,35 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     prefs?.setString(settingId, value); |     prefs?.setString(settingId, value); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Map<String, int> get categories => | ||||||
|  |       Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}')); | ||||||
|  |  | ||||||
|  |   set categories(Map<String, int> cats) { | ||||||
|  |     prefs?.setString('categories', jsonEncode(cats)); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool setEqual(Set<String> a, Set<String> b) => | ||||||
|  |       a.length == b.length && a.union(b).length == a.length; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ import 'package:obtainium/app_sources/steammobile.dart'; | |||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/mass_app_sources/githubstars.dart'; | import 'package:obtainium/mass_app_sources/githubstars.dart'; | ||||||
|  | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
|  |  | ||||||
| class AppNames { | class AppNames { | ||||||
|   late String author; |   late String author; | ||||||
| @@ -44,9 +45,10 @@ class App { | |||||||
|   late String latestVersion; |   late String latestVersion; | ||||||
|   List<String> apkUrls = []; |   List<String> apkUrls = []; | ||||||
|   late int preferredApkIndex; |   late int preferredApkIndex; | ||||||
|   late Map<String, String> additionalSettings; |   late Map<String, dynamic> additionalSettings; | ||||||
|   late DateTime? lastUpdateCheck; |   late DateTime? lastUpdateCheck; | ||||||
|   bool pinned = false; |   bool pinned = false; | ||||||
|  |   List<String> categories; | ||||||
|   App( |   App( | ||||||
|       this.id, |       this.id, | ||||||
|       this.url, |       this.url, | ||||||
| @@ -58,7 +60,8 @@ class App { | |||||||
|       this.preferredApkIndex, |       this.preferredApkIndex, | ||||||
|       this.additionalSettings, |       this.additionalSettings, | ||||||
|       this.lastUpdateCheck, |       this.lastUpdateCheck, | ||||||
|       this.pinned); |       this.pinned, | ||||||
|  |       {this.categories = const []}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
| @@ -69,24 +72,42 @@ class App { | |||||||
|     var source = SourceProvider().getSource(json['url']); |     var source = SourceProvider().getSource(json['url']); | ||||||
|     var formItems = source.combinedAppSpecificSettingFormItems |     var formItems = source.combinedAppSpecificSettingFormItems | ||||||
|         .reduce((value, element) => [...value, ...element]); |         .reduce((value, element) => [...value, ...element]); | ||||||
|     Map<String, String> additionalSettings = |     Map<String, dynamic> additionalSettings = | ||||||
|         getDefaultValuesFromFormItems([formItems]); |         getDefaultValuesFromFormItems([formItems]); | ||||||
|     if (json['additionalSettings'] != null) { |     if (json['additionalSettings'] != null) { | ||||||
|       additionalSettings.addEntries( |       additionalSettings.addEntries( | ||||||
|           Map<String, String>.from(jsonDecode(json['additionalSettings'])) |           Map<String, dynamic>.from(jsonDecode(json['additionalSettings'])) | ||||||
|               .entries); |               .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) { |     if (json['additionalData'] != null) { | ||||||
|       List<String> temp = List<String>.from(jsonDecode(json['additionalData'])); |       List<String> temp = List<String>.from(jsonDecode(json['additionalData'])); | ||||||
|       temp.asMap().forEach((i, value) { |       temp.asMap().forEach((i, value) { | ||||||
|         if (i < formItems.length) { |         if (i < formItems.length) { | ||||||
|           additionalSettings[formItems[i].key] = value; |           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'] = |       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]); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     int preferredApkIndex = json['preferredApkIndex'] == null | ||||||
|  |         ? 0 | ||||||
|  |         : json['preferredApkIndex'] as int; | ||||||
|  |     if (preferredApkIndex < 0) { | ||||||
|  |       preferredApkIndex = 0; | ||||||
|     } |     } | ||||||
|     return App( |     return App( | ||||||
|         json['id'] as String, |         json['id'] as String, | ||||||
| @@ -100,14 +121,19 @@ class App { | |||||||
|         json['apkUrls'] == null |         json['apkUrls'] == null | ||||||
|             ? [] |             ? [] | ||||||
|             : List<String>.from(jsonDecode(json['apkUrls'])), |             : List<String>.from(jsonDecode(json['apkUrls'])), | ||||||
|         json['preferredApkIndex'] == null |         preferredApkIndex, | ||||||
|             ? 0 |  | ||||||
|             : json['preferredApkIndex'] as int, |  | ||||||
|         additionalSettings, |         additionalSettings, | ||||||
|         json['lastUpdateCheck'] == null |         json['lastUpdateCheck'] == null | ||||||
|             ? null |             ? null | ||||||
|             : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), |             : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), | ||||||
|         json['pinned'] ?? false); |         json['pinned'] ?? false, | ||||||
|  |         categories: json['categories'] != null | ||||||
|  |             ? (json['categories'] as List<dynamic>) | ||||||
|  |                 .map((e) => e.toString()) | ||||||
|  |                 .toList() | ||||||
|  |             : json['category'] != null | ||||||
|  |                 ? [json['category'] as String] | ||||||
|  |                 : []); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, dynamic> toJson() => { |   Map<String, dynamic> toJson() => { | ||||||
| @@ -121,7 +147,8 @@ class App { | |||||||
|         'preferredApkIndex': preferredApkIndex, |         'preferredApkIndex': preferredApkIndex, | ||||||
|         'additionalSettings': jsonEncode(additionalSettings), |         'additionalSettings': jsonEncode(additionalSettings), | ||||||
|         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, |         'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, | ||||||
|         'pinned': pinned |         'pinned': pinned, | ||||||
|  |         'categories': categories | ||||||
|       }; |       }; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -155,7 +182,7 @@ List<String> getLinksFromParsedHTML( | |||||||
|         .map((e) => '$prependToLinks${e.attributes['href']!}') |         .map((e) => '$prependToLinks${e.attributes['href']!}') | ||||||
|         .toList(); |         .toList(); | ||||||
|  |  | ||||||
| Map<String, String> getDefaultValuesFromFormItems( | Map<String, dynamic> getDefaultValuesFromFormItems( | ||||||
|     List<List<GeneratedFormItem>> items) { |     List<List<GeneratedFormItem>> items) { | ||||||
|   return Map.fromEntries(items |   return Map.fromEntries(items | ||||||
|       .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) |       .map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? ''))) | ||||||
| @@ -176,7 +203,7 @@ class AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|       String standardUrl, Map<String, String> additionalSettings) { |       String standardUrl, Map<String, dynamic> additionalSettings) { | ||||||
|     throw NotImplementedError(); |     throw NotImplementedError(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -188,16 +215,12 @@ class AppSource { | |||||||
|   final List<List<GeneratedFormItem>> |   final List<List<GeneratedFormItem>> | ||||||
|       additionalAppSpecificSourceAgnosticSettingFormItems = [ |       additionalAppSpecificSourceAgnosticSettingFormItems = [ | ||||||
|     [ |     [ | ||||||
|       GeneratedFormItem( |       GeneratedFormSwitch( | ||||||
|         'trackOnly', |         'trackOnly', | ||||||
|         label: tr('trackOnly'), |         label: tr('trackOnly'), | ||||||
|         type: FormItemType.bool, |  | ||||||
|       ) |       ) | ||||||
|     ], |     ], | ||||||
|     [ |     [GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))] | ||||||
|       GeneratedFormItem('noVersionDetection', |  | ||||||
|           label: tr('noVersionDetection'), type: FormItemType.bool) |  | ||||||
|     ] |  | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   // Previous 2 variables combined into one at runtime for convenient usage |   // Previous 2 variables combined into one at runtime for convenient usage | ||||||
| @@ -225,7 +248,7 @@ class AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   String? tryInferringAppId(String standardUrl, |   String? tryInferringAppId(String standardUrl, | ||||||
|       {Map<String, String> additionalSettings = const {}}) { |       {Map<String, dynamic> additionalSettings = const {}}) { | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -288,7 +311,7 @@ class SourceProvider { | |||||||
|   bool ifRequiredAppSpecificSettingsExist(AppSource source) { |   bool ifRequiredAppSpecificSettingsExist(AppSource source) { | ||||||
|     for (var row in source.combinedAppSpecificSettingFormItems) { |     for (var row in source.combinedAppSpecificSettingFormItems) { | ||||||
|       for (var element in row) { |       for (var element in row) { | ||||||
|         if (element.required && element.opts == null) { |         if (element is GeneratedFormTextField && element.required) { | ||||||
|           return true; |           return true; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -314,17 +337,17 @@ class SourceProvider { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<App> getApp( |   Future<App> getApp( | ||||||
|       AppSource source, String url, Map<String, String> additionalSettings, |       AppSource source, String url, Map<String, dynamic> additionalSettings, | ||||||
|       {App? currentApp, |       {App? currentApp, | ||||||
|       bool trackOnlyOverride = false, |       bool trackOnlyOverride = false, | ||||||
|       noVersionDetectionOverride = false}) async { |       noVersionDetectionOverride = false}) async { | ||||||
|     if (trackOnlyOverride) { |     if (trackOnlyOverride || source.enforceTrackOnly) { | ||||||
|       additionalSettings['trackOnly'] = 'true'; |       additionalSettings['trackOnly'] = true; | ||||||
|     } |     } | ||||||
|     if (noVersionDetectionOverride) { |     if (noVersionDetectionOverride) { | ||||||
|       additionalSettings['noVersionDetection'] = 'true'; |       additionalSettings['noVersionDetection'] = true; | ||||||
|     } |     } | ||||||
|     var trackOnly = currentApp?.additionalSettings['trackOnly'] == 'true'; |     var trackOnly = additionalSettings['trackOnly'] == true; | ||||||
|     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); |     String standardUrl = source.standardizeURL(preStandardizeUrl(url)); | ||||||
|     APKDetails apk = |     APKDetails apk = | ||||||
|         await source.getLatestAPKDetails(standardUrl, additionalSettings); |         await source.getLatestAPKDetails(standardUrl, additionalSettings); | ||||||
| @@ -347,10 +370,11 @@ class SourceProvider { | |||||||
|         currentApp?.installedVersion, |         currentApp?.installedVersion, | ||||||
|         apkVersion, |         apkVersion, | ||||||
|         apk.apkUrls, |         apk.apkUrls, | ||||||
|         apk.apkUrls.length - 1, |         apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0, | ||||||
|         additionalSettings, |         additionalSettings, | ||||||
|         DateTime.now(), |         DateTime.now(), | ||||||
|         currentApp?.pinned ?? false); |         currentApp?.pinned ?? false, | ||||||
|  |         categories: currentApp?.categories ?? const []); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Returns errors in [results, errors] instead of throwing them |   // Returns errors in [results, errors] instead of throwing them | ||||||
|   | |||||||
| @@ -739,14 +739,14 @@ packages: | |||||||
|       name: webview_flutter |       name: webview_flutter | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.0" |     version: "4.0.1" | ||||||
|   webview_flutter_android: |   webview_flutter_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_android |       name: webview_flutter_android | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.1.1" | ||||||
|   webview_flutter_platform_interface: |   webview_flutter_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -760,7 +760,7 @@ packages: | |||||||
|       name: webview_flutter_wkwebview |       name: webview_flutter_wkwebview | ||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.1" | ||||||
|   win32: |   win32: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 0.8.23+87 # When changing this, update the tag in main() accordingly | version: 0.9.12+102 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user