Compare commits

...

100 Commits

Author SHA1 Message Date
5c4bb8f84c Merge pull request #217 from ImranR98/dev
Bugfixes and UI Tweaks (#213, #215, #216)
2023-01-06 21:11:58 -05:00
1c8e759494 Increment version + updated packages 2023-01-06 21:11:13 -05:00
081c2a07d2 Categories re-added on import (#213) 2023-01-06 21:10:04 -05:00
02751fe8fa Made GitHub PATs hidden (password field) (#215) 2023-01-06 20:57:26 -05:00
95f3362a84 Apps bottom bar tweaks (#216) 2023-01-06 20:47:22 -05:00
b68cf5a1be Increment version 2023-01-02 02:05:35 -05:00
4eb7499591 Merge pull request #211 from RanTranslations/main
assets: Update Simplified Chinese
2023-01-02 02:04:34 -05:00
98fafe2aa4 assets: Update Simplified Chinese 2023-01-02 11:18:27 +08:00
9bac74aadd Icon fixed in readme 2022-12-28 06:42:42 -05:00
0a93117bf0 Merge pull request #208 from ImranR98/dev
Tiny bugfix + increment version
2022-12-28 06:31:42 -05:00
451cc41c45 Tiny bugfix + increment version 2022-12-28 06:30:58 -05:00
3b449d0982 Merge pull request #207 from ImranR98/dev
Categorization Improvements
2022-12-27 22:39:17 -05:00
1863f55372 Increment build num 2022-12-27 22:38:07 -05:00
0c4b8ac79d Made notif icon white for consistency on some OS skins 2022-12-27 22:37:49 -05:00
e287087753 Increment build number 2022-12-27 21:15:06 -05:00
82bcc46d42 Fixed search error on Add App page (#202) 2022-12-27 21:14:11 -05:00
1f26188ec6 Potential fix for rangeError for no URL Apps (#201) 2022-12-27 21:00:46 -05:00
794c3e1a81 Increment version 2022-12-27 20:42:21 -05:00
16369b4adf App page with Webview now on par with no webview
+ ratelimit error bugfix
2022-12-27 20:41:44 -05:00
8f16f745be Added categorize in multi select menu 2022-12-27 20:15:56 -05:00
8ddeb3d776 Apps now support multiple categories 2022-12-27 19:37:13 -05:00
21cf9c98d9 Merge pull request #200 from ImranR98/dev
Fixed export error on Android SDK <= 28
2022-12-25 22:30:47 -05:00
358f910d19 Increment version 2022-12-25 22:30:01 -05:00
7a3d74bd05 Fixed export error on Android SDK <= 28 2022-12-25 22:29:39 -05:00
6f27f64699 Merge pull request #199 from ImranR98/dev
UI improvements
2022-12-25 21:56:36 -05:00
3341fecb68 Increment version 2022-12-25 21:53:26 -05:00
d3bce63ca4 Updated plugins 2022-12-25 21:53:06 -05:00
8aa8b6b698 Added selection count on Apps page 2022-12-25 21:52:21 -05:00
3d6c9bbf98 Added category multi-select to Apps filter
+ UI tweaks and bugfixes
2022-12-25 21:41:51 -05:00
7af0a8628c Slightly thicker category color indicator on apps page 2022-12-25 20:31:20 -05:00
4573ce6bcf Added category select to add app page 2022-12-25 20:30:36 -05:00
e29d38fa32 Adding an existing category no longer overwrites it 2022-12-25 20:04:47 -05:00
dc82431235 App page now scrollable when categories overflow 2022-12-25 19:58:58 -05:00
424b0028bf Merge pull request #198 from gidano/main
Update hu.json
2022-12-25 15:36:26 -05:00
46fba9e0a4 Update hu.json 2022-12-25 11:14:15 +01:00
b40be7569b Bugfix (#197) 2022-12-24 23:17:03 -05:00
a173be11eb Merge pull request #193 from ImranR98/dev
Track-only source bugfix +  better http errors
2022-12-23 23:53:08 -05:00
0c97b25d99 Track-only source bugfix + better http errors
+ increment version
2022-12-23 23:52:32 -05:00
f836fd20d8 Increment version 2022-12-22 17:43:08 -05:00
2f6917592d Merge pull request #190 from atilluF/Ita-TL
Update it.json
2022-12-22 17:39:47 -05:00
b864fef3ad Update it.json
New strings + fixes
2022-12-22 22:48:53 +01:00
8e487592b3 Increment version 2022-12-22 11:58:00 -05:00
e9a44746a5 Merge pull request #184 from gidano/main
Updated hu.json
2022-12-22 11:57:16 -05:00
9123737bf3 Merge branch 'main' into main 2022-12-22 14:58:25 +01:00
12f70951c2 Merge pull request #186 from bluefly000/japanese-translation
Update Japanese translation
2022-12-22 08:03:03 -05:00
c1d56f89f0 Merge pull request #187 from markus-gitdev/main
Update DE translation
2022-12-22 08:02:57 -05:00
4dfd29f5de Merge pull request #189 from ImranR98/dev
Bugfixes
2022-12-22 08:02:21 -05:00
226cfa25e0 Increment version 2022-12-22 08:01:52 -05:00
4e0c655538 F-Droid repo URL matching made more general (#188) 2022-12-22 08:01:26 -05:00
45a23e9025 Language fix for #185 2022-12-22 07:57:21 -05:00
1e5aa0999a Update DE translation
Update german translation to match newly added localized strings
2022-12-22 10:21:04 +01:00
beeec356e5 Update Japanese translation 2022-12-22 18:03:20 +09:00
01fa9a2e96 Updated hu.json 2022-12-22 09:18:32 +01:00
0da7a36f1a Merge pull request #183 from ImranR98/dev
Better Category UI + Language Setting
2022-12-22 03:14:43 -05:00
ed2a4e674f Added language setting (mostly working) - #165 2022-12-22 03:13:55 -05:00
0f6a683faa Increment version 2022-12-22 02:26:13 -05:00
fa4d46b622 Bugfix es+ new category picker on App page 2022-12-22 02:13:21 -05:00
a3f9947f28 Finished new category editor (needs to be used) 2022-12-22 01:24:35 -05:00
6977858b99 Started work on new unified category selector/editor 2022-12-21 23:54:36 -05:00
2ff6acb701 Merge pull request #182 from ImranR98/dev
Broke `GeneratedFormItem` into sub-types + bugfix
2022-12-21 18:26:15 -05:00
0c2d6ce84d Increment version 2022-12-21 18:23:55 -05:00
9072862862 Broke GeneratedFormItem into sub-types
Prep for "chips" input type
2022-12-21 18:23:25 -05:00
3cbaac2f5d Increment version 2022-12-21 15:08:18 -05:00
0f8871efcb Merge pull request #179 from bluefly000/japanese-translation
Update Japanese translation
2022-12-21 15:07:33 -05:00
ee216cbbba Merge pull request #181 from ImranR98/dev
Bugfix (#178) + translation typos
2022-12-21 15:07:26 -05:00
ebe5b79dc5 Bugfix (#178) + translation typos 2022-12-21 15:06:54 -05:00
60014c864c Update Japanese translation 2022-12-21 21:48:32 +09:00
070b6033bd Merge pull request #177 from ImranR98/dev
Added very basic categorization support
2022-12-21 04:25:45 -05:00
626bebbe5a Localized new strings 2022-12-21 04:24:17 -05:00
118460ccb9 Added category filter 2022-12-21 04:15:39 -05:00
26f953dbb0 Category displayed on App/Apps pages
+ category save bugfix
2022-12-21 03:57:08 -05:00
99d7595f2d Added category add/remove (no recolour/rename for now) 2022-12-21 03:08:56 -05:00
e2f99c5e71 Increment version 2022-12-20 21:12:50 -05:00
1f582d239b Merge pull request #175 from RanTranslations/main
assests: Update Simplified Chinese
2022-12-20 21:10:48 -05:00
5e6b00718e Merge pull request #176 from bluefly000/main
Update Japanese translation
2022-12-20 21:10:26 -05:00
56594e6b19 Update Japanese translation 2022-12-21 10:44:18 +09:00
bbcc3ff9b3 assests: Update Simplified Chinese 2022-12-21 09:07:31 +08:00
ee66c53320 Updated plugins 2022-12-20 19:06:49 -05:00
b7d581f8b0 GitHub prereleases now not included by default 2022-12-20 18:48:54 -05:00
ead63ba21d Translation typos 2022-12-20 18:41:02 -05:00
c69404363f Increment version 2022-12-20 18:34:47 -05:00
99d0bd2461 Merge pull request #173 from atilluF/main
Fix Italian translation
2022-12-20 18:33:55 -05:00
54efda3eea Merge pull request #171 from markus-gitdev/main
Improving german translation
2022-12-20 18:33:47 -05:00
d76d68329c Merge pull request #174 from ImranR98/dev
"Additional Settings" related code changes for maintainability + other changes
2022-12-20 18:32:46 -05:00
b151eb27e1 Translations + bugfix 2022-12-20 18:19:44 -05:00
6a21045e5b Progress 2022-12-20 18:00:22 -05:00
6aedd9ce37 Update it.json (small fixes) 2022-12-20 18:48:33 +01:00
f319639a99 Merge branch 'ImranR98:main' into main 2022-12-20 08:58:43 +01:00
92e6798809 Update de.json
Abbreviating some texts to provide a better appearance.
2022-12-20 08:56:46 +01:00
9a129d41df Added migration code for additionalData (NOTHING TESTED) 2022-12-19 20:14:54 -05:00
0c2654a226 More fixes to prev commit 2022-12-19 19:58:12 -05:00
afc8e41171 Made defaultvallue part of formitem 2022-12-19 19:48:37 -05:00
1fe9e4f91e Started switching additionaldata to map 2022-12-19 19:34:43 -05:00
dbd6dec0a6 Merge remote-tracking branch 'origin/main' into dev 2022-12-19 16:19:59 -05:00
d068db2a57 Increment version 2022-12-19 16:19:38 -05:00
dd5c5fd2bc Merge pull request #169 from markus-gitdev/main
German translation
2022-12-19 16:12:32 -05:00
ac9dadd9d0 Merge pull request #168 from gidano/main
HU Text correction
2022-12-19 16:11:50 -05:00
bb0540b644 German translation
Initial version of german translation
2022-12-19 10:05:21 +01:00
819334021a HU Text correction 2022-12-19 08:06:59 +01:00
63034dd3f9 Added 'no version detection' option 2022-12-18 02:46:25 -05:00
35 changed files with 2227 additions and 1112 deletions

View File

@ -1,4 +1,4 @@
# ![Obtainium Icon](./android/app/src/main/res/drawable/ic_notification.png) Obtainium # ![Obtainium Icon](./assets/graphics/icon_small.png) 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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

258
assets/translations/de.json Normal file
View File

@ -0,0 +1,258 @@
{
"invalidURLForSource": "Keine gültige {} App-URL",
"noReleaseFound": "Keine passende Version gefunden",
"noVersionFound": "Release-Version nicht ermittelbar",
"urlMatchesNoSource": "URL stimmt mit keiner bekannten Quelle überein",
"cantInstallOlderVersion": "Installation einer älteren App-Version nicht möglich",
"appIdMismatch": "Die heruntergeladene Paket-ID stimmt nicht mit der vorhandenen App-ID überein",
"functionNotImplemented": "Diese Klasse hat diese Funktion nicht implementiert",
"placeholder": "Platzhalter",
"someErrors": "Es traten einige Fehler auf",
"unexpectedError": "Unerwarteter Fehler",
"ok": "Okay",
"and": "und",
"startedBgUpdateTask": "Hintergrundaktualisierungsprüfung gestartet",
"bgUpdateIgnoreAfterIs": "Hintergrundaktualisierung 'ignoreAfter' ist {}",
"startedActualBGUpdateCheck": "Überprüfung der Hintergrundaktualisierung gestartet",
"bgUpdateTaskFinished": "Hintergrundaktualisierungsprüfung abgeschlossen",
"firstRun": "Dies ist der erste Start von Obtainium überhaupt",
"settingUpdateCheckIntervalTo": "Aktualisierungsintervall auf {} stellen",
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
"githubPATFormat": "Benutzername:Token",
"githubPATLinkText": "Über GitHub PATs",
"includePrereleases": "Vorabversionen einbeziehen",
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
"invalidRegEx": "Ungültiger regulärer Ausdruck",
"noDescription": "Keine Beschreibung",
"cancel": "Abbrechen",
"continue": "Weiter",
"requiredInBrackets": "(Benötigt)",
"dropdownNoOptsError": "FEHLER: DROPDOWN MUSS MINDESTENS EINE OPTION HABEN",
"colour": "Farbe",
"githubStarredRepos": "GitHub Starred Repos",
"uname": "Benutzername",
"wrongArgNum": "Falsche Anzahl von Argumenten übermittelt",
"xIsTrackOnly": "{} ist nur zur Nachverfolgung",
"source": "Quelle",
"app": "App",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.",
"youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.",
"trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
"cancelled": "Abgebrochen",
"appAlreadyAdded": "App bereits hinzugefügt",
"alreadyUpToDateQuestion": "App bereits auf dem neuesten Stand?",
"addApp": "App hinzufügen",
"appSourceURL": "Quell-URL der App",
"error": "Fehler",
"add": "Hinzufügen",
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
"search": "Suchen",
"additionalOptsFor": "Zusatzoptionen für {}",
"supportedSourcesBelow": "Unterstützte Quellen:",
"trackOnlyInBrackets": "(Nur Nachverfolgen)",
"searchableInBrackets": "(Durchsuchbar)",
"appsString": "Apps",
"noApps": "Keine Apps",
"noAppsForFilter": "Keine Apps für ausgewählten Filter",
"byX": "Von {}",
"percentProgress": "Fortschritt: {}%",
"pleaseWait": "Bitte warten",
"updateAvailable": "Aktualisierung verfügbar",
"estimateInBracketsShort": "(ca.)",
"notInstalled": "Nicht installiert",
"estimateInBrackets": "(Ungefähr)",
"selectAll": "Alle auswählen",
"deselectN": "{} abgewählt",
"xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
"removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
"removeSelectedApps": "Ausgewählte Apps entfernen",
"updateX": "Aktualisiere {}",
"installX": "Installiere {}",
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
"changeX": "Ändern {}",
"installUpdateApps": "Apps installieren/aktualisieren",
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
"onlyWorksWithNonEVDApps": "Funktioniert nur bei Apps, deren Installationsstatus nicht automatisch erkannt werden kann (ungewöhnlich).",
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
"no": "Nein",
"yes": "Ja",
"markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell",
"pinToTop": "Oben anheften",
"unpinFromTop": "'Oben anheften' aufheben",
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
"resetInstallStatus": "Installationsstatus zurücksetzen",
"more": "Mehr",
"removeOutdatedFilter": "App-Filter 'Nicht aktuell' entfernen",
"showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen",
"filter": "Filter",
"filterActive": "Filter *",
"filterApps": "Apps filtern",
"appName": "App Name",
"author": "Autor",
"upToDateApps": "Apps mit aktueller Version",
"nonInstalledApps": "Nicht installierte Apps",
"importExport": "Import/Export",
"settings": "Einstellungen",
"exportedTo": "Exportiert zu {}",
"obtainiumExport": "Obtainium Export",
"invalidInput": "Ungültige Eingabe",
"importedX": "Importiert {}",
"obtainiumImport": "Obtainium Import",
"importFromURLList": "Importieren aus URL-Liste",
"searchQuery": "Suchanfrage",
"appURLList": "App URL-Liste",
"line": "Linie",
"searchX": "Suche {}",
"noResults": "Keine Ergebnisse gefunden",
"importX": "Import {}",
"importedAppsIdDisclaimer": "Importierte Apps werden möglicherweise fälschlicherweise als \"Nicht installiert\" angezeigt. Um dies zu beheben, installieren Sie sie erneut über Obtainium. Dies hat keine Auswirkungen auf App-Daten. Es betrifft nur URL- und Drittanbieter-Importmethoden.",
"importErrors": "Importfehler",
"importedXOfYApps": "{} von {} Apps importiert.",
"followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
"okay": "Okay",
"selectURL": "URL auswählen",
"selectURLs": "URLs auswählen",
"pick": "Auswählen",
"theme": "Theme",
"dark": "Dunkel",
"light": "Hell",
"followSystem": "System folgen",
"obtainium": "Obtainium",
"materialYou": "Material You",
"appSortBy": "App sortieren nach",
"authorName": "Autor/Name",
"nameAuthor": "Name/Autor",
"asAdded": "Wie hinzugefügt",
"appSortOrder": "App Sortierung nach",
"ascending": "Aufsteigend",
"descending": "Absteigend",
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
"neverManualOnly": "Nie - nur manuell",
"appearance": "Aussehen",
"showWebInAppView": "Quellwebseite in der App-Ansicht anzeigen",
"pinUpdates": "Apps mit Aktualisierungen oben anheften",
"updates": "Aktualisierungen",
"sourceSpecific": "Quellenspezifisch",
"appSource": "App-Quelle",
"noLogs": "Keine Protokolle",
"appLogs": "App Protokolle",
"close": "Schließen",
"share": "Teilen",
"appNotFound": "App nicht gefunden",
"obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "APK auswählen",
"appHasMoreThanOnePackage": "{} verfügt über mehr als ein Paket:",
"deviceSupportsXArch": "Ihr Gerät unterstützt die CPU-Architektur {}.",
"deviceSupportsFollowingArchs": "Ihr Gerät unterstützt die folgenden CPU-Architekturen:",
"warning": "Warnung",
"sourceIsXButPackageFromYPrompt": "Die App-Quelle ist '{}', aber das Release-Paket stammt von '{}'. Fortfahren?",
"updatesAvailable": "Aktualisierungen verfügbar",
"updatesAvailableNotifDescription": "Benachrichtigt den Nutzer, dass Aktualisierungen für eine oder mehrere von Obtainium verfolgte Apps verfügbar sind",
"noNewUpdates": "Keine neuen Aktualisierungen.",
"xHasAnUpdate": "{} hat eine Aktualisierung.",
"appsUpdated": "Apps aktualisiert",
"appsUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Aktualisierungen für eine oder mehrere Apps im Hintergrund durchgeführt wurden",
"xWasUpdatedToY": "{} wurde auf {} aktualisiert.",
"errorCheckingUpdates": "Fehler beim Prüfen auf Aktualisierungen",
"errorCheckingUpdatesNotifDescription": "Eine Benachrichtigung, die angezeigt wird, wenn die Prüfung der Hintergrundaktualisierung fehlschlägt",
"appsRemoved": "Apps entfernt",
"appsRemovedNotifDescription": "Benachrichtigt den Benutzer, dass eine oder mehrere Apps aufgrund von Fehlern beim Laden entfernt wurden",
"xWasRemovedDueToErrorY": "{} wurde aufgrund des folgenden Fehlers entfernt: {}",
"completeAppInstallation": "App Installation abschließen",
"obtainiumMustBeOpenToInstallApps": "Obtainium muss geöffnet sein, um Apps zu installieren",
"completeAppInstallationNotifDescription": "Aufforderung an den Benutzer, zu Obtainium zurückzukehren, um die Installation einer App abzuschließen",
"checkingForUpdates": "Nach Aktualisierungen suchen",
"checkingForUpdatesNotifDescription": "Vorübergehende Benachrichtigung, die bei der Suche nach Aktualisierungen angezeigt wird",
"pleaseAllowInstallPerm": "Bitte erlauben Sie Obtainium die Installation von Apps",
"trackOnly": "Nur Nachverfolgen",
"errorWithHttpStatusCode": "Fehler {}",
"versionCorrectionDisabled": "Versionskorrektur deaktiviert (Plugin scheint nicht zu funktionieren)",
"unknown": "Unbekannt",
"none": "Keine",
"never": "Nie",
"latestVersionX": "Neueste Version: {}",
"installedVersionX": "Installierte Version: {}",
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
"remove": "Entfernen",
"removeAppQuestion": "App entfernen?",
"yesMarkUpdated": "Ja, als aktualisiert markieren",
"fdroid": "F-Droid",
"appIdOrName": "App ID oder Name",
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
"install": "Installieren",
"markInstalled": "Als Installiert markieren",
"update": "Aktualisieren",
"markUpdated": "Als Aktuell markieren",
"additionalOptions": "Zusätzliche Optionen",
"disableVersionDetection": "Versionsermittlung deaktivieren",
"noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.",
"downloadingX": "Lade {} herunter",
"downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App",
"noAPKFound": "Keine APK gefunden",
"noVersionDetection": "Keine Versionserkennung",
"categorize": "Kategorisieren",
"categories": "Kategorien",
"category": "Kategorie",
"noCategory": "Keine Kategorie",
"noCategories": "Keine Kategorien",
"deleteCategoriesQuestion": "Kategorien löschen?",
"categoryDeleteWarning": "Alle Apps in gelöschten Kategorien werden auf nicht kategorisiert gesetzt.",
"addCategory": "Kategorie hinzufügen",
"label": "Bezeichnung",
"language": "Sprache",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": {
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minute geplant",
"other": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minuten geplant"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "Hintergrundaktualisierungsprüfung fand {} Aktualisierung - benachrichtigt den Benutzer, falls erforderlich",
"other": "Hintergrundaktualisierungsprüfung fand {} Aktualisierungen - benachrichtigt den Benutzer, falls erforderlich"
},
"apps": {
"one": "{} App",
"other": "{} Apps"
},
"url": {
"one": "{} URL",
"other": "{} URLs"
},
"minute": {
"one": "{} Minute",
"other": "{} Minutes"
},
"hour": {
"one": "{} Stunde",
"other": "{} Stunden"
},
"day": {
"one": "{} Tag",
"other": "{} Tage"
},
"clearedNLogsBeforeXAfterY": {
"one": "{n} Protokoll gelöscht (vorher = {vorher}, nachher = {nachher})",
"other": "{n} Protokolle gelöscht (vorher = {vorher}, nachher = {nachher})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} und 1 weitere App haben Aktualisierungen.",
"other": "{} und {} weitere Apps haben Aktualisierungen."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
}
}

View File

@ -37,7 +37,7 @@
"xIsTrackOnly": "{} is Track-Only", "xIsTrackOnly": "{} is Track-Only",
"source": "Source", "source": "Source",
"app": "App", "app": "App",
"appsFromSourceAreTrackOnly": "Apps from this source are 'Track-Only'.' ", "appsFromSourceAreTrackOnly": "Apps from this source are 'Track-Only'.",
"youPickedTrackOnly": "You have selected the 'Track-Only' option.", "youPickedTrackOnly": "You have selected the 'Track-Only' option.",
"trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.", "trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.",
"cancelled": "Cancelled", "cancelled": "Cancelled",
@ -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",
@ -188,6 +188,29 @@
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Install",
"markInstalled": "Mark Installed",
"update": "Update",
"markUpdated": "Mark Updated",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Disable Version Detection",
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
"downloadingX": "Downloading {}",
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
"noAPKFound": "No APK found",
"noVersionDetection": "No version detection",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"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"

View File

@ -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,16 +28,16 @@
"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'.",
"youPickedTrackOnly": "A 'Csak követés' opciót választotta.", "youPickedTrackOnly": "A 'Csak követés' opciót választotta.",
"trackOnlyAppDescription": "Az alkalmazás frissítéseit nyomon követi, de az Obtainium nem tudja letölteni vagy telepíteni.", "trackOnlyAppDescription": "Az alkalmazás frissítéseit nyomon követi, de az Obtainium nem tudja letölteni vagy telepíteni.",
"cancelled": "Törölve", "cancelled": "Törölve",
@ -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 webhely 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,10 +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": "Telepít",
"markInstalled": "Telepítettnek jelöl",
"update": "Frissít",
"markUpdated": "Frissítettnek jelöl",
"additionalOptions": "További lehetőségek",
"disableVersionDetection": "Verzióérzékelés letiltása",
"noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.",
"downloadingX": "{} letöltés",
"downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról",
"noAPKFound": "Nem található APK",
"noVersionDetection": "Nincs verzió érzékelés",
"categorize": "Kategorizálás",
"categories": "Kategóriák",
"category": "Kategória",
"noCategory": "Nincs kategória",
"deleteCategoryQuestion": "Törli a kategóriát?",
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
"addCategory": "Új kategória",
"label": "Címke",
"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"
@ -232,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."
} }
} }

View File

@ -16,38 +16,38 @@
"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",
"includePrereleases": "Includi prerelease", "includePrereleases": "Includi prerelease",
"fallbackToOlderReleases": "Ripiega su release datate", "fallbackToOlderReleases": "Ripiega su release precedenti",
"filterReleaseTitlesByRegEx": "Filtra le release con le espressioni regolari", "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
"invalidRegEx": "Espressione regolare invalida", "invalidRegEx": "Espressione regolare non valida",
"noDescription": "Descrizione assente", "noDescription": "Descrizione assente",
"cancel": "Annulla", "cancel": "Annulla",
"continue": "Continua", "continue": "Continua",
"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",
"add": "Aggiungi", "add": "Aggiungi",
"searchSomeSourcesLabel": "Cerca (disponibile solo per alcune fonti)", "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
"search": "Cerca", "search": "Cerca",
"additionalOptsFor": "Opzioni aggiuntive per {}", "additionalOptsFor": "Opzioni aggiuntive per {}",
"supportedSourcesBelow": "Fonti supportate:", "supportedSourcesBelow": "Fonti supportate:",
@ -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",
@ -86,8 +86,8 @@
"shareSelectedAppURLs": "Condividi gli URL delle App selezionate", "shareSelectedAppURLs": "Condividi gli URL delle App selezionate",
"resetInstallStatus": "Ripristina lo stato d'installazione", "resetInstallStatus": "Ripristina lo stato d'installazione",
"more": "Di più", "more": "Di più",
"removeOutdatedFilter": "Rimuovi il filtro per le App datate", "removeOutdatedFilter": "Rimuovi il filtro per le App non aggiornate",
"showOutdatedOnly": "Mostra solo le App datate", "showOutdatedOnly": "Mostra solo le App non aggiornate",
"filter": "Filtri", "filter": "Filtri",
"filterActive": "Filtri *", "filterActive": "Filtri *",
"filterApps": "Filtra App", "filterApps": "Filtra App",
@ -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",
@ -106,7 +106,7 @@
"searchQuery": "Stringa di ricerca", "searchQuery": "Stringa di ricerca",
"appURLList": "Lista di URL delle App", "appURLList": "Lista di URL delle App",
"line": "Linea", "line": "Linea",
"searchX": "Cerca {}", "searchX": "Cerca su {}",
"noResults": "Nessun risultato trovato", "noResults": "Nessun risultato trovato",
"importX": "Importa {}", "importX": "Importa {}",
"importedAppsIdDisclaimer": "Le App importate potrebbero essere visualizzate erroneamente come \"Non installate\".\nPer risolvere il problema, reinstallale con Obtainium.\nQuesto non dovrebbe influire sui dati delle App.\n\nRiguarda solo l'URL e i metodi di importazione di terze parti.", "importedAppsIdDisclaimer": "Le App importate potrebbero essere visualizzate erroneamente come \"Non installate\".\nPer risolvere il problema, reinstallale con Obtainium.\nQuesto non dovrebbe influire sui dati delle App.\n\nRiguarda solo l'URL e i metodi di importazione di terze parti.",
@ -120,7 +120,7 @@
"theme": "Tema", "theme": "Tema",
"dark": "Scuro", "dark": "Scuro",
"light": "Chiaro", "light": "Chiaro",
"followSystem": "Segui il sistema", "followSystem": "Segui sistema",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"appSortBy": "App ordinate per", "appSortBy": "App ordinate per",
@ -133,10 +133,10 @@
"bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in background", "bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in background",
"neverManualOnly": "Mai - Solo manuale", "neverManualOnly": "Mai - Solo manuale",
"appearance": "Aspetto", "appearance": "Aspetto",
"showWebInAppView": "Mostra la pagina web dell'App se selezionata", "showWebInAppView": "Mostra pagina web dell'App se selezionata",
"pinUpdates": "Fissa in alto gli aggiornamenti disponibili nella pagina delle App", "pinUpdates": "Fissa aggiornamenti disponibili in alto",
"updates": "Aggiornato", "updates": "Aggiornamenti",
"sourceSpecific": "Specifico per la fonte", "sourceSpecific": "Specifiche per la fonte",
"appSource": "Sorgente dell'App", "appSource": "Sorgente dell'App",
"noLogs": "Nessun log", "noLogs": "Nessun log",
"appLogs": "Log dell'App", "appLogs": "Log dell'App",
@ -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,27 +178,50 @@
"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",
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome", "appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
"reposHaveMultipleApps": "I repository possono contenere più App", "reposHaveMultipleApps": "I repository possono contenere più App",
"fdroidThirdPartyRepo": "Repository di terze parti di F-Droid", "fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Installa",
"markInstalled": "Contrassegna come installato",
"update": "Aggiorna",
"markUpdated": "Contrassegna come aggiornato",
"additionalOptions": "Opzioni aggiuntive",
"disableVersionDetection": "Disattiva il rilevamento della versione",
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le App la cui versione non viene rilevata correttamente.",
"downloadingX": "Scaricamento di {} in corso",
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'App",
"noAPKFound": "Nessun APK trovato",
"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",

View File

@ -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,18 +27,18 @@
"invalidRegEx": "無効な正規表現", "invalidRegEx": "無効な正規表現",
"noDescription": "説明はありません", "noDescription": "説明はありません",
"cancel": "キャンセル", "cancel": "キャンセル",
"continue": "続ける", "continue": "続",
"requiredInBrackets": "(必須)", "requiredInBrackets": "(必須)",
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのoptが必要です", "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
"colour": "カラー", "colour": "カラー",
"githubStarredRepos": "Githubでスターしたリポジトリ", "githubStarredRepos": "Githubでスターしたリポジトリ",
"uname": "ユーザー名", "uname": "ユーザー名",
"wrongArgNum": "提供する引数の数が間違っています", "wrongArgNum": "提供する引数の数が間違っています",
"xIsTrackOnly": "{} は'追跡のみ'です", "xIsTrackOnly": "{} は追跡のみです",
"source": "ソース", "source": "ソース",
"app": "アプリ", "app": "アプリ",
"appsFromSourceAreTrackOnly": "このソースからのアプリは'追跡のみ'です'。", "appsFromSourceAreTrackOnly": "このソースからのアプリは追跡のみです。",
"youPickedTrackOnly": "'追跡のみ'を選択しています", "youPickedTrackOnly": "追跡のみを選択しています",
"trackOnlyAppDescription": "アプリのアップデートは追跡されますが、Obtainiumはアプリのダウンロードやインストールをすることはできません。", "trackOnlyAppDescription": "アプリのアップデートは追跡されますが、Obtainiumはアプリのダウンロードやインストールをすることはできません。",
"cancelled": "キャンセルしました", "cancelled": "キャンセルしました",
"appAlreadyAdded": "アプリはすでに追加されています", "appAlreadyAdded": "アプリはすでに追加されています",
@ -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アーキテクチャをサポートしています:",
@ -152,11 +152,11 @@
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?", "sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
"updatesAvailable": "アップデートが利用可能", "updatesAvailable": "アップデートが利用可能",
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
"noNewUpdates": "新しいアップデートはありません", "noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{}のアップデートが利用可能です", "xHasAnUpdate": "{}のアップデートが利用可能です",
"appsUpdated": "アプリをアップデートしました", "appsUpdated": "アプリをアップデートしました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{}が{}にアップデートされました", "xWasUpdatedToY": "{}が{}にアップデートされました",
"errorCheckingUpdates": "アップデート確認中のエラー", "errorCheckingUpdates": "アップデート確認中のエラー",
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知", "errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
"appsRemoved": "削除されたアプリ", "appsRemoved": "削除されたアプリ",
@ -173,7 +173,7 @@
"versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)", "versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)",
"unknown": "不明", "unknown": "不明",
"none": "なし", "none": "なし",
"never": "Never", "never": "しない",
"latestVersionX": "最新のバージョン: {}", "latestVersionX": "最新のバージョン: {}",
"installedVersionX": "インストールされたバージョン: {}", "installedVersionX": "インストールされたバージョン: {}",
"lastUpdateCheckX": "最終アップデート確認: {}", "lastUpdateCheckX": "最終アップデート確認: {}",
@ -181,24 +181,47 @@
"removeAppQuestion": "アプリを削除しますか?", "removeAppQuestion": "アプリを削除しますか?",
"yesMarkUpdated": "はい、アップデート済みとしてマークします", "yesMarkUpdated": "はい、アップデート済みとしてマークします",
"fdroid": "F-Droid", "fdroid": "F-Droid",
"appIdOrName": "アプリIDまたは名前", "appIdOrName": "アプリIDまたは名前",
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした", "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります", "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo", "fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "インストール",
"markInstalled": "インストール済みとしてマークする",
"update": "アップデート",
"markUpdated": "アップデート済みとしてマークする",
"additionalOptions": "追加のオプション",
"disableVersionDetection": "バージョン検出を無効にする",
"noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。",
"downloadingX": "{} をダウンロード中",
"downloadNotifDescription": "アプリのダウンロード状況を通知する",
"noAPKFound": "APKが見つかりません",
"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": "{}個のアプリ",
@ -225,11 +248,11 @@
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})" "other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{}とさらに{}個のアプリのアップデートが利用可能です", "one": "{}とさらに{}個のアプリのアップデートが利用可能です",
"other": "{}とさらに{}個のアプリのアップデートが利用可能です" "other": "{}とさらに{}個のアプリのアップデートが利用可能です"
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{}とさらに{}個のアプリがアップデートされました", "one": "{}とさらに{}個のアプリがアップデートされました",
"other": "{}とさらに{}個のアプリがアップデートされました" "other": "{}とさらに{}個のアプリがアップデートされました"
} }
} }

View File

@ -12,7 +12,7 @@
"ok": "好的", "ok": "好的",
"and": "和", "and": "和",
"startedBgUpdateTask": "开始后台检查更新任务", "startedBgUpdateTask": "开始后台检查更新任务",
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}", "bgUpdateIgnoreAfterIs": "下次后台更新检查 {}",
"startedActualBGUpdateCheck": "后台检查更新已开始", "startedActualBGUpdateCheck": "后台检查更新已开始",
"bgUpdateTaskFinished": "后台检查更新已完成", "bgUpdateTaskFinished": "后台检查更新已完成",
"firstRun": "这是你第一次运行 Obtainium", "firstRun": "这是你第一次运行 Obtainium",
@ -22,9 +22,9 @@
"githubPATFormat": "username:token", "githubPATFormat": "username:token",
"githubPATLinkText": "关于 GitHub 个人访问令牌", "githubPATLinkText": "关于 GitHub 个人访问令牌",
"includePrereleases": "包含预发布版", "includePrereleases": "包含预发布版",
"fallbackToOlderReleases": "回到旧版", "fallbackToOlderReleases": "回退到旧版",
"filterReleaseTitlesByRegEx": "通过正则表达式过滤发布标题", "filterReleaseTitlesByRegEx": "使用正则以过滤发布标题",
"invalidRegEx": "无效的正则表达式", "invalidRegEx": "表达式无效",
"noDescription": "无描述", "noDescription": "无描述",
"cancel": "取消", "cancel": "取消",
"continue": "继续", "continue": "继续",
@ -109,7 +109,7 @@
"searchX": "搜索 {}", "searchX": "搜索 {}",
"noResults": "无结果", "noResults": "无结果",
"importX": "导入 {}", "importX": "导入 {}",
"importedAppsIdDisclaimer": "导入的应用程序可能不正确地显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。", "importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
"importErrors": "导入错误", "importErrors": "导入错误",
"importedXOfYApps": "{} 中的 {} 个应用已导入", "importedXOfYApps": "{} 中的 {} 个应用已导入",
"followingURLsHadErrors": "以下 URL 有错误:", "followingURLsHadErrors": "以下 URL 有错误:",
@ -134,7 +134,7 @@
"neverManualOnly": "手动", "neverManualOnly": "手动",
"appearance": "外观", "appearance": "外观",
"showWebInAppView": "在应用来源页显示网页", "showWebInAppView": "在应用来源页显示网页",
"pinUpdates": "将需要更新的应用固定到顶部", "pinUpdates": "更新的应用置顶",
"updates": "检查间隔", "updates": "检查间隔",
"sourceSpecific": "Github 访问令牌", "sourceSpecific": "Github 访问令牌",
"appSource": "源代码", "appSource": "源代码",
@ -145,9 +145,9 @@
"appNotFound": "未找到应用", "appNotFound": "未找到应用",
"obtainiumExportHyphenatedLowercase": "obtainium-导出", "obtainiumExportHyphenatedLowercase": "obtainium-导出",
"pickAnAPK": "选择一个安装包", "pickAnAPK": "选择一个安装包",
"appHasMoreThanOnePackage": "{} 有多于一个安装包", "appHasMoreThanOnePackage": "{} 有多个架构可用",
"deviceSupportsXArch": "你的设备支持 {} CPU 架构", "deviceSupportsXArch": "你的设备支持 {} 架构",
"deviceSupportsFollowingArchs": "你的设备支持以下 CPU 架构:", "deviceSupportsFollowingArchs": "你的设备支持以下架构:",
"warning": "警告", "warning": "警告",
"sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?", "sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?",
"updatesAvailable": "更新可用", "updatesAvailable": "更新可用",
@ -188,6 +188,29 @@
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "安装",
"markInstalled": "标记为已安装",
"update": "更新",
"markUpdated": "标记为已更新",
"additionalOptions": "附加选项",
"disableVersionDetection": "关闭版本检测",
"noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
"downloadingX": "下载中 {}",
"downloadNotifDescription": "通知用户下载进度",
"noAPKFound": "未找到安装包",
"noVersionDetection": "无版本检测",
"categorize": "归档",
"categories": "归档",
"category": "类别",
"noCategory": "无类别",
"noCategories": "无类别",
"deleteCategoriesQuestion": "删除所有类别?",
"categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
"addCategory": "添加类别",
"label": "标签",
"language": "语言",
"storagePermissionDenied": "存储权限已被拒绝",
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "请求过多 (API 限制) - 在 {} 分钟后重试", "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
"other": "请求过多 (API 限制) - 在 {} 分钟后重试" "other": "请求过多 (API 限制) - 在 {} 分钟后重试"

View File

@ -25,8 +25,9 @@ class APKMirror extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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) {
String? titleString = parse(res.body) String? titleString = parse(res.body)
@ -45,7 +46,7 @@ class APKMirror extends AppSource {
} }
return APKDetails(version, [], getAppNames(standardUrl)); return APKDetails(version, [], getAppNames(standardUrl));
} else { } else {
throw NoReleasesError(); throw getObtainiumHttpError(res);
} }
} }

View File

@ -32,7 +32,7 @@ class FDroid extends AppSource {
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) { {Map<String, dynamic> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last; return Uri.parse(standardUrl).pathSegments.last;
} }
@ -54,14 +54,15 @@ 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, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')), await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),

View File

@ -9,13 +9,12 @@ class FDroidRepo extends AppSource {
FDroidRepo() { FDroidRepo() {
name = tr('fdroidThirdPartyRepo'); name = tr('fdroidThirdPartyRepo');
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem( GeneratedFormTextField('appIdOrName',
label: tr('appIdOrName'), label: tr('appIdOrName'),
hint: tr('reposHaveMultipleApps'), hint: tr('reposHaveMultipleApps'),
required: true, required: true)
key: 'appIdOrName')
] ]
]; ];
} }
@ -23,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,13 +32,10 @@ class FDroidRepo extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
String? appIdOrName = findGeneratedFormValueByKey( ) async {
additionalSourceAppSpecificFormItems String? appIdOrName = additionalSettings['appIdOrName'];
.reduce((value, element) => [...value, ...element]),
additionalData,
'appIdOrName');
if (appIdOrName == null) { if (appIdOrName == null) {
throw NoReleasesError(); throw NoReleasesError();
} }
@ -84,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);
} }
} }
} }

View File

@ -12,12 +12,10 @@ class GitHub extends AppSource {
GitHub() { GitHub() {
host = 'github.com'; host = 'github.com';
additionalSourceAppSpecificDefaults = ['true', 'true', ''];
additionalSourceSpecificSettingFormItems = [ additionalSourceSpecificSettingFormItems = [
GeneratedFormItem( GeneratedFormTextField('github-creds',
label: tr('githubPATLabel'), label: tr('githubPATLabel'),
id: 'github-creds', password: true,
required: false, required: false,
additionalValidators: [ additionalValidators: [
(value) { (value) {
@ -52,19 +50,18 @@ class GitHub extends AppSource {
]) ])
]; ];
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem( GeneratedFormSwitch('includePrereleases',
label: tr('includePrereleases'), type: FormItemType.bool) label: tr('includePrereleases'), defaultValue: false)
], ],
[ [
GeneratedFormItem( GeneratedFormSwitch('fallbackToOlderReleases',
label: tr('fallbackToOlderReleases'), type: FormItemType.bool) label: tr('fallbackToOlderReleases'), defaultValue: true)
], ],
[ [
GeneratedFormItem( GeneratedFormTextField('filterReleaseTitlesByRegEx',
label: tr('filterReleaseTitlesByRegEx'), label: tr('filterReleaseTitlesByRegEx'),
type: FormItemType.string,
required: false, required: false,
additionalValidators: [ additionalValidators: [
(value) { (value) {
@ -99,7 +96,7 @@ class GitHub extends AppSource {
SettingsProvider settingsProvider = SettingsProvider(); SettingsProvider settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings(); await settingsProvider.initializeSettings();
String? creds = settingsProvider String? creds = settingsProvider
.getSettingString(additionalSourceSpecificSettingFormItems[0].id); .getSettingString(additionalSourceSpecificSettingFormItems[0].key);
return creds != null && creds.isNotEmpty ? '$creds@' : ''; return creds != null && creds.isNotEmpty ? '$creds@' : '';
} }
@ -109,15 +106,18 @@ class GitHub extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
var includePrereleases = ) async {
additionalData.isNotEmpty && additionalData[0] == 'true'; bool includePrereleases = additionalSettings['includePrereleases'];
var fallbackToOlderReleases = bool fallbackToOlderReleases =
additionalData.length >= 2 && additionalData[1] == 'true'; additionalSettings['fallbackToOlderReleases'];
var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty String? regexFilter =
? additionalData[2] (additionalSettings['filterReleaseTitlesByRegEx'] as String?)
: null; ?.isNotEmpty ==
true
? additionalSettings['filterReleaseTitlesByRegEx']
: null;
Response res = await get(Uri.parse( Response res = await get(Uri.parse(
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases')); 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -148,7 +148,7 @@ class GitHub extends AppSource {
continue; continue;
} }
var apkUrls = getReleaseAPKUrls(releases[i]); var apkUrls = getReleaseAPKUrls(releases[i]);
if (apkUrls.isEmpty && !trackOnly) { if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
continue; continue;
} }
targetRelease = releases[i]; targetRelease = releases[i];

View File

@ -25,8 +25,9 @@ class GitLab extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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) {
var standardUri = Uri.parse(standardUrl); var standardUri = Uri.parse(standardUrl);
@ -58,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);
} }
} }
} }

View File

@ -23,14 +23,15 @@ class IzzyOnDroid extends AppSource {
@override @override
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) { {Map<String, dynamic> additionalSettings = const {}}) {
return FDroid().tryInferringAppId(standardUrl); return FDroid().tryInferringAppId(standardUrl);
} }
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
await get( await get(

View File

@ -24,8 +24,9 @@ class Mullvad extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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) {
var version = parse(res.body) var version = parse(res.body)
@ -42,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);
} }
} }
} }

View File

@ -18,8 +18,9 @@ class Signal extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -32,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);
} }
} }
} }

View File

@ -23,8 +23,9 @@ class SourceForge extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);
@ -56,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);
} }
} }
} }

View File

@ -9,14 +9,8 @@ class SteamMobile extends AppSource {
SteamMobile() { SteamMobile() {
host = 'store.steampowered.com'; host = 'store.steampowered.com';
name = tr('steam'); name = tr('steam');
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [GeneratedFormDropdown('app', apks.entries.toList(), label: tr('app'))]
GeneratedFormItem(
label: tr('app'),
key: 'app',
required: true,
opts: apks.entries.toList())
]
]; ];
} }
@ -32,15 +26,12 @@ class SteamMobile extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) 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 = findGeneratedFormValueByKey( var apkNamePrefix = additionalSettings['app'] as String?;
additionalSourceAppSpecificFormItems
.reduce((value, element) => [...value, ...element]),
additionalData,
'app');
if (apkNamePrefix == null) { if (apkNamePrefix == null) {
throw NoReleasesError(); throw NoReleasesError();
} }
@ -63,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);
} }
} }
} }

View File

@ -1,74 +1,168 @@
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';
enum FormItemType { string, bool } abstract class GeneratedFormItem {
typedef OnValueChanges = void Function(
List<String> values, bool valid, bool isBuilding);
class GeneratedFormItem {
late String key; late String key;
late String label; late String label;
late FormItemType type; late List<Widget> belowWidgets;
late dynamic defaultValue;
List<dynamic> additionalValidators;
dynamic ensureType(dynamic val);
GeneratedFormItem(this.key,
{this.label = 'Input',
this.belowWidgets = const [],
this.defaultValue,
this.additionalValidators = const []});
}
class GeneratedFormTextField extends GeneratedFormItem {
late bool required; late bool required;
late int max; late int max;
late List<String? Function(String? value)> additionalValidators;
late String id;
late List<Widget> belowWidgets;
late String? hint; late String? hint;
late List<MapEntry<String, String>>? opts; late bool password;
GeneratedFormItem( GeneratedFormTextField(String key,
{this.label = 'Input', {String label = 'Input',
this.type = FormItemType.string, 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.id = 'input',
this.belowWidgets = const [],
this.hint, this.hint,
this.opts, this.password = false})
this.key = 'default'}) { : super(key,
if (type != FormItemType.string) { label: label,
required = false; belowWidgets: belowWidgets,
} defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override
String ensureType(val) {
return val.toString();
} }
} }
class GeneratedFormDropdown extends GeneratedFormItem {
late List<MapEntry<String, String>>? opts;
GeneratedFormDropdown(
String key,
this.opts, {
String label = 'Input',
List<Widget> belowWidgets = const [],
String defaultValue = '',
List<String? Function(String? value)> additionalValidators = const [],
}) : super(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override
String ensureType(val) {
return val.toString();
}
}
class GeneratedFormSwitch extends GeneratedFormItem {
GeneratedFormSwitch(
String key, {
String label = 'Input',
List<Widget> belowWidgets = const [],
bool defaultValue = false,
List<String? Function(bool value)> additionalValidators = const [],
}) : super(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override
bool ensureType(val) {
return val == true || val == 'true';
}
}
class GeneratedFormTagInput extends GeneratedFormItem {
late MapEntry<String, String>? deleteConfirmationMessage;
late bool singleSelect;
late WrapAlignment alignment;
late String emptyMessage;
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, {super.key, required this.items, required this.onValueChanges});
required this.items,
required this.onValueChanges,
required this.defaultValues});
final List<List<GeneratedFormItem>> items; final List<List<GeneratedFormItem>> items;
final OnValueChanges onValueChanges; final OnValueChanges onValueChanges;
final List<String> defaultValues;
@override @override
State<GeneratedForm> createState() => _GeneratedFormState(); State<GeneratedForm> createState() => _GeneratedFormState();
} }
// Generates a random light color
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
Color generateRandomLightColor() {
// Create a random number generator
final Random random = Random();
// Generate random hue, saturation, and value values
final double hue = random.nextDouble() * 360;
final double saturation = 0.5 + random.nextDouble() * 0.5;
final double value = 0.9 + random.nextDouble() * 0.1;
// Create a HSV color with the random values
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
}
class _GeneratedFormState extends State<GeneratedForm> { class _GeneratedFormState extends State<GeneratedForm> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
late List<List<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}) {
List<String> returnValues = []; Map<String, dynamic> returnValues = values;
var valid = true; var valid = true;
for (int r = 0; r < values.length; r++) { for (int r = 0; r < widget.items.length; r++) {
for (int i = 0; i < values[r].length; i++) { for (int i = 0; i < widget.items[r].length; i++) {
returnValues.add(values[r][i]);
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); }
} }
} }
} }
@ -80,42 +174,44 @@ class _GeneratedFormState extends State<GeneratedForm> {
super.initState(); super.initState();
// Initialize form values as all empty // Initialize form values as all empty
values.clear();
int j = 0; int j = 0;
values = widget.items for (var row in widget.items) {
.map((row) => row.map((e) { for (var e in row) {
return j < widget.defaultValues.length values[e.key] = e.defaultValue;
? widget.defaultValues[j++] }
: e.opts != null }
? e.opts!.first.key
: '';
}).toList())
.toList();
// 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(
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
key: formFieldKey, key: formFieldKey,
initialValue: values[row.key][e.key], initialValue: values[formItem.key],
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
values[row.key][e.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;
@ -124,21 +220,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[row.key][e.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[row.key][e.key] = value ?? e.value.opts!.first.key; values[formItem.key] = value ?? formItem.opts!.first.key;
someValueChanged(); someValueChanged();
}); });
}); });
@ -154,21 +249,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[r][e] == 'true', value: values[widget.items[r][e].key],
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
values[r][e] = 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'),
)),
],
)
]);
} }
} }
} }
@ -178,9 +453,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,
) )
@ -217,18 +491,3 @@ class _GeneratedFormState extends State<GeneratedForm> {
)); ));
} }
} }
String? findGeneratedFormValueByKey(
List<GeneratedFormItem> items, List<String> values, String key) {
var foundIndex = -1;
for (var i = 0; i < items.length; i++) {
if (items[i].key == key) {
foundIndex = i;
break;
}
}
if (foundIndex >= 0 && foundIndex < values.length) {
return values[foundIndex];
}
return null;
}

View File

@ -8,28 +8,29 @@ class GeneratedFormModal extends StatefulWidget {
{super.key, {super.key,
required this.title, required this.title,
required this.items, required this.items,
required this.defaultValues,
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 List<String> defaultValues;
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> {
List<String> values = []; Map<String, dynamic> values = {};
bool valid = false; bool valid = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
values = widget.defaultValues;
valid = widget.initValid || widget.items.isEmpty; valid = widget.initValid || widget.items.isEmpty;
} }
@ -57,25 +58,29 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
this.valid = valid; this.valid = valid;
}); });
} }
}, }),
defaultValues: widget.defaultValues) 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()
], ],
); );
} }

View File

@ -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 {

View File

@ -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.20'; const String currentVersion = '0.9.14';
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
@ -32,7 +32,8 @@ const supportedLocales = [
Locale('zh'), Locale('zh'),
Locale('it'), Locale('it'),
Locale('ja'), Locale('ja'),
Locale('hu') Locale('hu'),
Locale('de')
]; ];
const fallbackLocale = Locale('en'); const fallbackLocale = Locale('en');
const localeDir = 'assets/translations'; const localeDir = 'assets/translations';
@ -42,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) {
@ -159,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()),
)); ));
} }
@ -199,9 +205,8 @@ class _ObtainiumState extends State<Obtainium> {
currentReleaseTag, currentReleaseTag,
[], [],
0, 0,
['true'], {'includePrereleases': true},
null, null,
false,
false) false)
]); ]);
} }

View File

@ -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,10 +28,9 @@ class _AddAppPageState extends State<AddAppPage> {
String userInput = ''; String userInput = '';
String searchQuery = ''; String searchQuery = '';
AppSource? pickedSource; AppSource? pickedSource;
List<String> sourceSpecificAdditionalData = []; Map<String, dynamic> additionalSettings = {};
bool sourceSpecificDataIsValid = true; bool additionalSettingsValid = true;
List<String> otherAdditionalData = []; List<String> pickedCategories = [];
bool otherAdditionalDataIsValid = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -39,23 +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;
sourceSpecificAdditionalData =
source != null ? source.additionalSourceAppSpecificDefaults : [];
sourceSpecificDataIsValid = source != null
? !sourceProvider.ifSourceAppsRequireAdditionalData(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,11 +62,9 @@ class _AddAppPageState extends State<AddAppPage> {
}); });
var settingsProvider = context.read<SettingsProvider>(); var settingsProvider = context.read<SettingsProvider>();
() async { () async {
var userPickedTrackOnly = findGeneratedFormValueByKey( var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
pickedSource!.additionalAppSpecificSourceAgnosticFormItems, var userPickedNoVersionDetection =
otherAdditionalData, additionalSettings['noVersionDetection'] == true;
'trackOnlyFormItemKey') ==
'true';
var cont = true; var cont = true;
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) && if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
await showDialog( await showDialog(
@ -83,7 +77,6 @@ class _AddAppPageState extends State<AddAppPage> {
: tr('app') : tr('app')
]), ]),
items: const [], items: const [],
defaultValues: const [],
message: message:
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}', '${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
); );
@ -91,17 +84,32 @@ class _AddAppPageState extends State<AddAppPage> {
null) { null) {
cont = false; cont = false;
} }
if (userPickedNoVersionDetection &&
await showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('disableVersionDetection'),
items: const [],
message: tr('noVersionDetectionExplanation'),
);
}) ==
null) {
cont = false;
}
if (cont) { if (cont) {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
App app = await sourceProvider.getApp( App app = await sourceProvider.getApp(
pickedSource!, userInput, sourceSpecificAdditionalData, pickedSource!, userInput, additionalSettings,
trackOnly: trackOnly); trackOnlyOverride: trackOnly,
noVersionDetectionOverride: userPickedNoVersionDetection);
if (!trackOnly) { if (!trackOnly) {
await settingsProvider.getInstallPermission(); await settingsProvider.getInstallPermission();
} }
// 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) && !app.trackOnly) { if (sourceProvider.isTempId(app.id) &&
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) {
@ -116,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.trackOnly) { 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;
@ -156,34 +165,33 @@ class _AddAppPageState extends State<AddAppPage> {
Expanded( Expanded(
child: GeneratedForm( child: GeneratedForm(
items: [ items: [
[ [
GeneratedFormItem( GeneratedFormTextField('appSourceURL',
label: tr('appSourceURL'), label: tr('appSourceURL'),
additionalValidators: [ additionalValidators: [
(value) { (value) {
try { try {
sourceProvider sourceProvider
.getSource(value ?? '') .getSource(value ?? '')
.standardizeURL( .standardizeURL(
preStandardizeUrl( preStandardizeUrl(
value ?? '')); value ?? ''));
} catch (e) { } catch (e) {
return e is String return e is String
? e ? e
: e is ObtainiumError : e is ObtainiumError
? e.toString() ? e.toString()
: tr('error'); : tr('error');
} }
return null; return null;
} }
]) ])
] ]
], ],
onValueChanges: (values, valid, isBuilding) { onValueChanges: (values, valid, isBuilding) {
changeUserInput( changeUserInput(values['appSourceURL']!,
values[0], valid, isBuilding); valid, isBuilding);
}, })),
defaultValues: const [])),
const SizedBox( const SizedBox(
width: 16, width: 16,
), ),
@ -193,13 +201,9 @@ class _AddAppPageState extends State<AddAppPage> {
onPressed: gettingAppInfo || onPressed: gettingAppInfo ||
pickedSource == null || pickedSource == null ||
(pickedSource! (pickedSource!
.additionalSourceAppSpecificFormItems .combinedAppSpecificSettingFormItems
.isNotEmpty && .isNotEmpty &&
!sourceSpecificDataIsValid) || !additionalSettingsValid)
(pickedSource!
.additionalAppSpecificSourceAgnosticDefaults
.isNotEmpty &&
!otherAdditionalDataIsValid)
? null ? null
: addApp, : addApp,
child: Text(tr('add'))) child: Text(tr('add')))
@ -224,19 +228,22 @@ class _AddAppPageState extends State<AddAppPage> {
child: GeneratedForm( child: GeneratedForm(
items: [ items: [
[ [
GeneratedFormItem( 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 = values[0].trim(); searchQuery =
values['searchSomeSources']!.trim();
}); });
} }
}, }),
defaultValues: const ['']),
), ),
const SizedBox( const SizedBox(
width: 16, width: 16,
@ -281,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) {
@ -291,16 +298,7 @@ class _AddAppPageState extends State<AddAppPage> {
child: Text(tr('search'))) child: Text(tr('search')))
], ],
), ),
if (pickedSource != null && if (pickedSource != null)
(pickedSource!.additionalSourceAppSpecificDefaults
.isNotEmpty ||
pickedSource!
.additionalAppSpecificSourceAgnosticFormItems
.where((e) => pickedSource!.enforceTrackOnly
? e.key != 'trackOnlyFormItemKey'
: true)
.map((e) => [e])
.isNotEmpty))
Column( Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -316,46 +314,29 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
if (pickedSource!
.additionalSourceAppSpecificFormItems
.isNotEmpty)
GeneratedForm(
items: pickedSource!
.additionalSourceAppSpecificFormItems,
onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) {
setState(() {
sourceSpecificAdditionalData = values;
sourceSpecificDataIsValid = valid;
});
}
},
defaultValues: pickedSource!
.additionalSourceAppSpecificDefaults),
if (pickedSource!
.additionalAppSpecificSourceAgnosticDefaults
.isNotEmpty)
const SizedBox(
height: 8,
),
GeneratedForm( GeneratedForm(
items: pickedSource! items: pickedSource!
.additionalAppSpecificSourceAgnosticFormItems .combinedAppSpecificSettingFormItems,
.where((e) => pickedSource!.enforceTrackOnly
? e.key != 'trackOnlyFormItemKey'
: true)
.map((e) => [e])
.toList(),
onValueChanges: (values, valid, isBuilding) { onValueChanges: (values, valid, isBuilding) {
if (!isBuilding) { if (!isBuilding) {
setState(() { setState(() {
otherAdditionalData = values; additionalSettings = values;
otherAdditionalDataIsValid = valid; additionalSettingsValid = valid;
}); });
} }
}, }),
defaultValues: pickedSource! Column(
.additionalAppSpecificSourceAgnosticDefaults), children: [
const SizedBox(
height: 16,
),
CategoryEditorSelector(
alignment: WrapAlignment.start,
onSelected: (categories) {
pickedCategories = categories;
}),
],
),
], ],
) )
else else

View File

@ -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,98 +41,137 @@ 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 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,
body: RefreshIndicator( body: RefreshIndicator(
child: settingsProvider.showAppWebpage child: settingsProvider.showAppWebpage
? WebView( ? app != null
backgroundColor: Theme.of(context).colorScheme.background, ? WebViewWidget(
initialUrl: app?.app.url, controller: WebViewController()
javascriptMode: JavascriptMode.unrestricted, ..setJavaScriptMode(JavaScriptMode.unrestricted)
) ..setBackgroundColor(
: CustomScrollView( Theme.of(context).colorScheme.background)
slivers: [ ..setJavaScriptMode(JavaScriptMode.unrestricted)
SliverFillRemaining( ..setNavigationDelegate(
child: Column( NavigationDelegate(
mainAxisAlignment: MainAxisAlignment.center, onWebResourceError: (WebResourceError error) {
crossAxisAlignment: CrossAxisAlignment.stretch, if (error.isForMainFrame == true) {
children: [ showError(
app?.installedInfo != null ObtainiumError(error.description,
? Row( unexpected: true),
mainAxisAlignment: MainAxisAlignment.center, context);
children: [
Image.memory(
app!.installedInfo!.icon!,
height: 150,
gaplessPlayback: true,
)
])
: Container(),
const SizedBox(
height: 25,
),
Text(
app?.installedInfo?.name ?? app?.app.name ?? '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')
])}${app?.app.trackOnly == true ? ' ${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),
) )
], ..loadRequest(Uri.parse(app.app.url)))
)), : Container()
: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(children: [fullInfoColumn])),
], ],
), ),
onRefresh: () async { onRefresh: () async {
@ -151,7 +191,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
if (app?.app.installedVersion != null && if (app?.app.installedVersion != null &&
app?.app.trackOnly == false && !trackOnly &&
app?.app.installedVersion != app?.app.latestVersion) app?.app.installedVersion != app?.app.latestVersion)
IconButton( IconButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null
@ -199,30 +239,48 @@ class _AppPageState extends State<AppPage> {
); );
}); });
}, },
tooltip: 'Mark as Updated', tooltip: tr('markUpdated'),
icon: const Icon(Icons.done)), icon: const Icon(Icons.done)),
if (source != null && if (source != null &&
source.additionalSourceAppSpecificFormItems source
.isNotEmpty) .combinedAppSpecificSettingFormItems.isNotEmpty)
IconButton( IconButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null
? null ? null
: () { : () {
showDialog<List<String>>( showDialog<Map<String, dynamic>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
var items = source
.combinedAppSpecificSettingFormItems
.map((row) {
row.map((e) {
if (app?.app.additionalSettings[
e.key] !=
null) {
e.defaultValue = app?.app
.additionalSettings[
e.key];
}
return e;
}).toList();
return row;
}).toList();
return GeneratedFormModal( return GeneratedFormModal(
title: 'Additional Options', title: tr('additionalOptions'),
items: source items: items);
.additionalSourceAppSpecificFormItems,
defaultValues: app != null
? app.app.additionalData
: source
.additionalSourceAppSpecificDefaults);
}).then((values) { }).then((values) {
if (app != null && values != null) { if (app != null && values != null) {
var changedApp = app.app; var changedApp = app.app;
changedApp.additionalData = values; changedApp.additionalSettings =
values;
if (source.enforceTrackOnly) {
changedApp.additionalSettings[
'trackOnly'] = true;
showError(
tr('appsFromSourceAreTrackOnly'),
context);
}
appsProvider.saveApps( appsProvider.saveApps(
[changedApp]).then((value) { [changedApp]).then((value) {
getUpdate(changedApp.id); getUpdate(changedApp.id);
@ -230,8 +288,33 @@ class _AppPageState extends State<AppPage> {
} }
}); });
}, },
tooltip: 'Additional Options', 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(
@ -242,7 +325,9 @@ class _AppPageState extends State<AppPage> {
? () { ? () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
() async { () async {
if (app?.app.trackOnly != true) { if (app?.app.additionalSettings[
'trackOnly'] !=
true) {
await settingsProvider await settingsProvider
.getInstallPermission(); .getInstallPermission();
} }
@ -264,12 +349,12 @@ class _AppPageState extends State<AppPage> {
} }
: null, : null,
child: Text(app?.app.installedVersion == null child: Text(app?.app.installedVersion == null
? app?.app.trackOnly == false ? !trackOnly
? 'Install' ? tr('install')
: 'Mark Installed' : tr('markInstalled')
: app?.app.trackOnly == false : !trackOnly
? 'Update' ? tr('update')
: 'Mark Updated'))), : tr('markUpdated')))),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
ElevatedButton( ElevatedButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import 'package:obtainium/components/generated_form.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/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var outlineButtonStyle = ButtonStyle( var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
StadiumBorder( StadiumBorder(
@ -66,6 +68,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')))),
@ -98,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> {
appsProvider appsProvider
.importApps(data) .importApps(data)
.then((value) { .then((value) {
var cats =
settingsProvider.categories;
appsProvider.apps
.forEach((key, value) {
for (var c
in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] =
generateRandomLightColor()
.value;
}
}
});
settingsProvider.categories =
cats;
showError( showError(
tr('importedX', args: [ tr('importedX', args: [
plural('apps', value) plural('apps', value)
@ -138,18 +157,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( 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
@ -172,12 +192,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
]) ])
] ]
], ],
defaultValues: const [],
); );
}).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;
}); });
@ -225,7 +245,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) {
@ -236,22 +257,26 @@ class _ImportExportPageState extends State<ImportExportPage> {
]), ]),
items: [ items: [
[ [
GeneratedFormItem( GeneratedFormTextField(
'searchQuery',
label: tr( label: tr(
'searchQuery')) 'searchQuery'))
] ]
], ],
defaultValues: const [],
); );
}); });
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 =
@ -332,7 +357,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) {
@ -346,10 +373,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
.requiredArgs .requiredArgs
.map( .map(
(e) => [ (e) => [
GeneratedFormItem(label: e) GeneratedFormTextField(e,
label: e)
]) ])
.toList(), .toList(),
defaultValues: const [],
); );
}); });
if (values != null) { if (values != null) {
@ -359,7 +386,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>?>(

View File

@ -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,21 +180,17 @@ 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) {
for (var i = 0; i < values.length; i++) { values.forEach((key, value) {
settingsProvider.setSettingString( settingsProvider.setSettingString(key, value);
e.additionalSourceSpecificSettingFormItems[i].id, });
values[i]);
}
} }
}, });
defaultValues: e.additionalSourceSpecificSettingFormItems.map((e) {
return settingsProvider.getSettingString(e.id) ?? '';
}).toList());
} else { } else {
return Container(); return Container();
} }
@ -195,6 +233,8 @@ class _SettingsPageState extends State<SettingsPage> {
], ],
), ),
height16, height16,
localeDropdown,
height16,
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -237,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(
@ -351,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());
}
}
}));
}
}

View File

@ -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,8 @@ class AppsProvider with ChangeNotifier {
throw ObtainiumError(tr('appNotFound')); throw ObtainiumError(tr('appNotFound'));
} }
String? apkUrl; String? apkUrl;
if (!apps[id]!.app.trackOnly) { var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
if (!trackOnly) {
apkUrl = await confirmApkUrl(apps[id]!.app, context); apkUrl = await confirmApkUrl(apps[id]!.app, context);
} }
if (apkUrl != null) { if (apkUrl != null) {
@ -326,7 +328,7 @@ class AppsProvider with ChangeNotifier {
appsToInstall.add(id); appsToInstall.add(id);
} }
} }
if (apps[id]!.app.trackOnly) { if (trackOnly) {
trackOnlyAppsToUpdate.add(id); trackOnlyAppsToUpdate.add(id);
} }
} }
@ -451,9 +453,10 @@ 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;
if (installedInfo == null && var trackOnly = app.additionalSettings['trackOnly'] == true;
app.installedVersion != null && var noVersionDetection =
!app.trackOnly) { app.additionalSettings['noVersionDetection'] == true;
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
app.installedVersion = null; app.installedVersion = null;
modded = true; modded = true;
} else if (installedInfo?.versionName != null && } else if (installedInfo?.versionName != null &&
@ -461,7 +464,8 @@ class AppsProvider with ChangeNotifier {
app.installedVersion = installedInfo!.versionName; app.installedVersion = installedInfo!.versionName;
modded = true; modded = true;
} else if (installedInfo?.versionName != null && } else if (installedInfo?.versionName != null &&
installedInfo!.versionName != app.installedVersion) { installedInfo!.versionName != app.installedVersion &&
!noVersionDetection) {
String? correctedInstalledVersion = reconcileRealAndInternalVersions( String? correctedInstalledVersion = reconcileRealAndInternalVersions(
installedInfo.versionName!, app.installedVersion!); installedInfo.versionName!, app.installedVersion!);
if (correctedInstalledVersion != null) { if (correctedInstalledVersion != null) {
@ -470,7 +474,8 @@ class AppsProvider with ChangeNotifier {
} }
} }
if (app.installedVersion != null && if (app.installedVersion != null &&
app.installedVersion != app.latestVersion) { app.installedVersion != app.latestVersion &&
!noVersionDetection) {
app.installedVersion = reconcileRealAndInternalVersions( app.installedVersion = reconcileRealAndInternalVersions(
app.installedVersion!, app.latestVersion, app.installedVersion!, app.latestVersion,
matchMode: true) ?? matchMode: true) ??
@ -623,12 +628,8 @@ class AppsProvider with ChangeNotifier {
App newApp = await sourceProvider.getApp( App newApp = await sourceProvider.getApp(
sourceProvider.getSource(currentApp.url), sourceProvider.getSource(currentApp.url),
currentApp.url, currentApp.url,
currentApp.additionalData, currentApp.additionalSettings,
name: currentApp.name, currentApp: currentApp);
id: currentApp.id,
pinned: currentApp.pinned,
trackOnly: currentApp.trackOnly,
installedVersion: currentApp.installedVersion);
if (currentApp.preferredApkIndex < newApp.apkUrls.length) { if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
newApp.preferredApkIndex = currentApp.preferredApkIndex; newApp.preferredApkIndex = currentApp.preferredApkIndex;
} }
@ -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(

View File

@ -80,11 +80,11 @@ class DownloadNotification extends ObtainiumNotification {
DownloadNotification(String appName, int progPercent) DownloadNotification(String appName, int progPercent)
: super( : super(
appName.hashCode, appName.hashCode,
'Downloading $appName', tr('downloadingX', args: [appName]),
'', '',
'APP_DOWNLOADING', 'APP_DOWNLOADING',
'Downloading App', tr('downloadingX', args: [tr('app')]),
'Notifies the user of the progress in downloading an App', tr('downloadNotifDescription'),
Importance.low, Importance.low,
onlyAlertOnce: true, onlyAlertOnce: true,
progPercent: progPercent); progPercent: progPercent);

View File

@ -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;
} }

View File

@ -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,10 +45,10 @@ class App {
late String latestVersion; late String latestVersion;
List<String> apkUrls = []; List<String> apkUrls = [];
late int preferredApkIndex; late int preferredApkIndex;
late List<String> additionalData; late Map<String, dynamic> additionalSettings;
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
bool pinned = false; bool pinned = false;
bool trackOnly = false; List<String> categories;
App( App(
this.id, this.id,
this.url, this.url,
@ -57,39 +58,83 @@ class App {
this.latestVersion, this.latestVersion,
this.apkUrls, this.apkUrls,
this.preferredApkIndex, this.preferredApkIndex,
this.additionalData, this.additionalSettings,
this.lastUpdateCheck, this.lastUpdateCheck,
this.pinned, this.pinned,
this.trackOnly); {this.categories = const []});
@override @override
String toString() { String toString() {
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned'; return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.toString()} LASTCHECK: ${lastUpdateCheck.toString()} PINNED $pinned';
} }
factory App.fromJson(Map<String, dynamic> json) => App( factory App.fromJson(Map<String, dynamic> json) {
json['id'] as String, var source = SourceProvider().getSource(json['url']);
json['url'] as String, var formItems = source.combinedAppSpecificSettingFormItems
json['author'] as String, .reduce((value, element) => [...value, ...element]);
json['name'] as String, Map<String, dynamic> additionalSettings =
json['installedVersion'] == null getDefaultValuesFromFormItems([formItems]);
? null if (json['additionalSettings'] != null) {
: json['installedVersion'] as String, additionalSettings.addEntries(
json['latestVersion'] as String, Map<String, dynamic>.from(jsonDecode(json['additionalSettings']))
json['apkUrls'] == null .entries);
? [] }
: List<String>.from(jsonDecode(json['apkUrls'])), // If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int, if (json['additionalData'] != null) {
json['additionalData'] == null List<String> temp = List<String>.from(jsonDecode(json['additionalData']));
? SourceProvider() temp.asMap().forEach((i, value) {
.getSource(json['url']) if (i < formItems.length) {
.additionalSourceAppSpecificDefaults if (formItems[i] is GeneratedFormSwitch) {
: List<String>.from(jsonDecode(json['additionalData'])), additionalSettings[formItems[i].key] = value == 'true';
json['lastUpdateCheck'] == null } else {
? null additionalSettings[formItems[i].key] = value;
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), }
json['pinned'] ?? false, }
json['trackOnly'] ?? false); });
additionalSettings['trackOnly'] =
json['trackOnly'] == 'true' || json['trackOnly'] == true;
additionalSettings['noVersionDetection'] =
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(
json['id'] as String,
json['url'] as String,
json['author'] as String,
json['name'] as String,
json['installedVersion'] == null
? null
: json['installedVersion'] as String,
json['latestVersion'] as String,
json['apkUrls'] == null
? []
: List<String>.from(jsonDecode(json['apkUrls'])),
preferredApkIndex,
additionalSettings,
json['lastUpdateCheck'] == null
? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
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() => {
'id': id, 'id': id,
@ -100,10 +145,10 @@ class App {
'latestVersion': latestVersion, 'latestVersion': latestVersion,
'apkUrls': jsonEncode(apkUrls), 'apkUrls': jsonEncode(apkUrls),
'preferredApkIndex': preferredApkIndex, 'preferredApkIndex': preferredApkIndex,
'additionalData': jsonEncode(additionalData), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned,
'trackOnly': trackOnly 'categories': categories
}; };
} }
@ -124,7 +169,7 @@ preStandardizeUrl(String url) {
return url; return url;
} }
const String noAPKFound = 'No APK found'; String noAPKFound = tr('noAPKFound');
List<String> getLinksFromParsedHTML( List<String> getLinksFromParsedHTML(
Document dom, RegExp hrefPattern, String prependToLinks) => Document dom, RegExp hrefPattern, String prependToLinks) =>
@ -137,6 +182,13 @@ List<String> getLinksFromParsedHTML(
.map((e) => '$prependToLinks${e.attributes['href']!}') .map((e) => '$prependToLinks${e.attributes['href']!}')
.toList(); .toList();
Map<String, dynamic> getDefaultValuesFromFormItems(
List<List<GeneratedFormItem>> items) {
return Map.fromEntries(items
.map((row) => row.map((el) => MapEntry(el.key, el.defaultValue ?? '')))
.reduce((value, element) => [...value, ...element]));
}
class AppSource { class AppSource {
String? host; String? host;
late String name; late String name;
@ -151,23 +203,33 @@ class AppSource {
} }
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl, Map<String, dynamic> additionalSettings) {
{bool trackOnly = false}) {
throw NotImplementedError(); throw NotImplementedError();
} }
// Different Sources may need different kinds of additional data for Apps // Different Sources may need different kinds of additional data for Apps
List<List<GeneratedFormItem>> additionalSourceAppSpecificFormItems = []; List<List<GeneratedFormItem>> additionalSourceAppSpecificSettingFormItems =
List<String> additionalSourceAppSpecificDefaults = []; [];
// Some additional data may be needed for Apps regardless of Source // Some additional data may be needed for Apps regardless of Source
final List<GeneratedFormItem> additionalAppSpecificSourceAgnosticFormItems = [ final List<List<GeneratedFormItem>>
GeneratedFormItem( additionalAppSpecificSourceAgnosticSettingFormItems = [
[
GeneratedFormSwitch(
'trackOnly',
label: tr('trackOnly'), label: tr('trackOnly'),
type: FormItemType.bool, )
key: 'trackOnlyFormItemKey') ],
[GeneratedFormSwitch('noVersionDetection', label: tr('noVersionDetection'))]
]; ];
final List<String> additionalAppSpecificSourceAgnosticDefaults = [''];
// Previous 2 variables combined into one at runtime for convenient usage
List<List<GeneratedFormItem>> get combinedAppSpecificSettingFormItems {
return [
...additionalSourceAppSpecificSettingFormItems,
...additionalAppSpecificSourceAgnosticSettingFormItems
];
}
// Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider // Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = []; List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
@ -186,7 +248,7 @@ class AppSource {
} }
String? tryInferringAppId(String standardUrl, String? tryInferringAppId(String standardUrl,
{List<String> additionalData = const []}) { {Map<String, dynamic> additionalSettings = const {}}) {
return null; return null;
} }
} }
@ -246,10 +308,10 @@ class SourceProvider {
return source; return source;
} }
bool ifSourceAppsRequireAdditionalData(AppSource source) { bool ifRequiredAppSpecificSettingsExist(AppSource source) {
for (var row in source.additionalSourceAppSpecificFormItems) { 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;
} }
} }
@ -274,37 +336,45 @@ class SourceProvider {
return true; return true;
} }
Future<App> getApp(AppSource source, String url, List<String> additionalData, Future<App> getApp(
{String name = '', AppSource source, String url, Map<String, dynamic> additionalSettings,
String? id, {App? currentApp,
bool pinned = false, bool trackOnlyOverride = false,
bool trackOnly = false, noVersionDetectionOverride = false}) async {
String? installedVersion}) async { if (trackOnlyOverride || source.enforceTrackOnly) {
additionalSettings['trackOnly'] = true;
}
if (noVersionDetectionOverride) {
additionalSettings['noVersionDetection'] = true;
}
var trackOnly = additionalSettings['trackOnly'] == true;
String standardUrl = source.standardizeURL(preStandardizeUrl(url)); String standardUrl = source.standardizeURL(preStandardizeUrl(url));
APKDetails apk = await source APKDetails apk =
.getLatestAPKDetails(standardUrl, additionalData, trackOnly: trackOnly); await source.getLatestAPKDetails(standardUrl, additionalSettings);
if (apk.apkUrls.isEmpty && !trackOnly) { if (apk.apkUrls.isEmpty && !trackOnly) {
throw NoAPKError(); throw NoAPKError();
} }
String apkVersion = apk.version.replaceAll('/', '-'); String apkVersion = apk.version.replaceAll('/', '-');
var name = currentApp?.name.trim() ??
apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
return App( return App(
id ?? currentApp?.id ??
source.tryInferringAppId(standardUrl, source.tryInferringAppId(standardUrl,
additionalData: additionalData) ?? additionalSettings: additionalSettings) ??
generateTempID(apk.names, source), generateTempID(apk.names, source),
standardUrl, standardUrl,
apk.names.author[0].toUpperCase() + apk.names.author.substring(1), apk.names.author[0].toUpperCase() + apk.names.author.substring(1),
name.trim().isNotEmpty name.trim().isNotEmpty
? name ? name
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1), : apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
installedVersion, currentApp?.installedVersion,
apkVersion, apkVersion,
apk.apkUrls, apk.apkUrls,
apk.apkUrls.length - 1, apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0,
additionalData, additionalSettings,
DateTime.now(), DateTime.now(),
pinned, currentApp?.pinned ?? false,
trackOnly); categories: currentApp?.categories ?? const []);
} }
// Returns errors in [results, errors] instead of throwing them // Returns errors in [results, errors] instead of throwing them
@ -316,7 +386,10 @@ class SourceProvider {
try { try {
var source = getSource(url); var source = getSource(url);
apps.add(await getApp( apps.add(await getApp(
source, url, source.additionalSourceAppSpecificDefaults)); source,
url,
getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)));
} catch (e) { } catch (e) {
errors.addAll(<String, dynamic>{url: e}); errors.addAll(<String, dynamic>{url: e});
} }

View File

@ -56,7 +56,7 @@ packages:
name: checked_yaml name: checked_yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@ -182,7 +182,7 @@ packages:
name: file_picker name: file_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.2.3" version: "5.2.4"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -215,14 +215,14 @@ packages:
name: flutter_local_notifications name: flutter_local_notifications
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "12.0.4" version: "13.0.0"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_linux name: flutter_local_notifications_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "3.0.0"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -258,7 +258,7 @@ packages:
name: fluttertoast name: fluttertoast
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.1.1" version: "8.1.2"
html: html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -286,7 +286,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.2" version: "3.3.0"
install_plugin_v2: install_plugin_v2:
dependency: "direct main" dependency: "direct main"
description: description:
@ -510,7 +510,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.4" version: "6.0.5"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -552,14 +552,14 @@ packages:
name: shared_preferences_linux name: shared_preferences_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
shared_preferences_macos: shared_preferences_macos:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_macos name: shared_preferences_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -580,7 +580,7 @@ packages:
name: shared_preferences_windows name: shared_preferences_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -599,7 +599,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.2.2"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -655,7 +655,7 @@ packages:
name: timezone name: timezone
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0" version: "0.9.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -739,35 +739,35 @@ packages:
name: webview_flutter name: webview_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.4" 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: "2.10.4" version: "3.1.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.5" version: "2.0.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.9.5" version: "3.0.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -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.20+84 # When changing this, update the tag in main() accordingly version: 0.9.14+104 # 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'
@ -38,10 +38,10 @@ dependencies:
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
path_provider: ^2.0.11 path_provider: ^2.0.11
flutter_fgbg: ^0.2.0 # Try removing reliance on this flutter_fgbg: ^0.2.0 # Try removing reliance on this
flutter_local_notifications: ^12.0.0 flutter_local_notifications: ^13.0.0
provider: ^6.0.3 provider: ^6.0.3
http: ^0.13.5 http: ^0.13.5
webview_flutter: ^3.0.4 webview_flutter: ^4.0.0
dynamic_color: ^1.5.4 dynamic_color: ^1.5.4
html: ^0.15.0 html: ^0.15.0
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15