mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-14 13:46:43 +02:00
Compare commits
93 Commits
v0.8.13-be
...
v0.9.8-bet
Author | SHA1 | Date | |
---|---|---|---|
b40be7569b | |||
a173be11eb | |||
0c97b25d99 | |||
f836fd20d8 | |||
2f6917592d | |||
b864fef3ad | |||
8e487592b3 | |||
e9a44746a5 | |||
9123737bf3 | |||
12f70951c2 | |||
c1d56f89f0 | |||
4dfd29f5de | |||
226cfa25e0 | |||
4e0c655538 | |||
45a23e9025 | |||
1e5aa0999a | |||
beeec356e5 | |||
01fa9a2e96 | |||
0da7a36f1a | |||
ed2a4e674f | |||
0f6a683faa | |||
fa4d46b622 | |||
a3f9947f28 | |||
6977858b99 | |||
2ff6acb701 | |||
0c2d6ce84d | |||
9072862862 | |||
3cbaac2f5d | |||
0f8871efcb | |||
ee216cbbba | |||
ebe5b79dc5 | |||
60014c864c | |||
070b6033bd | |||
626bebbe5a | |||
118460ccb9 | |||
26f953dbb0 | |||
99d7595f2d | |||
e2f99c5e71 | |||
1f582d239b | |||
5e6b00718e | |||
56594e6b19 | |||
bbcc3ff9b3 | |||
ee66c53320 | |||
b7d581f8b0 | |||
ead63ba21d | |||
c69404363f | |||
99d0bd2461 | |||
54efda3eea | |||
d76d68329c | |||
b151eb27e1 | |||
6a21045e5b | |||
6aedd9ce37 | |||
f319639a99 | |||
92e6798809 | |||
9a129d41df | |||
0c2654a226 | |||
afc8e41171 | |||
1fe9e4f91e | |||
dbd6dec0a6 | |||
d068db2a57 | |||
dd5c5fd2bc | |||
ac9dadd9d0 | |||
bb0540b644 | |||
819334021a | |||
8ece0bbef9 | |||
6a41283e74 | |||
e6d5c7db3e | |||
d4c016d8ee | |||
63034dd3f9 | |||
67b986de93 | |||
aafe4bc515 | |||
e524335900 | |||
77751fa03f | |||
b4e06ffb8e | |||
af511deeca | |||
71c6db9510 | |||
8fac67c9e9 | |||
c317f23741 | |||
12c0dd8489 | |||
1c7385ab56 | |||
b46347a6e3 | |||
a7104c89dc | |||
347d2c2738 | |||
cc17260e54 | |||
1985dcec3a | |||
d435481f0b | |||
a68d49c71c | |||
2b6a16637e | |||
e46e4e5dbc | |||
848c8eaf5e | |||
ebc48169a1 | |||
54c37641d5 | |||
05ad01bf85 |
@ -15,7 +15,9 @@ 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)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
||||||
|
256
assets/translations/de.json
Normal file
256
assets/translations/de.json
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
@ -185,6 +185,30 @@
|
|||||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
||||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Install",
|
||||||
|
"markInstalled": "Mark Installed",
|
||||||
|
"update": "Update",
|
||||||
|
"markUpdated": "Mark Updated",
|
||||||
|
"additionalOptions": "Additional Options",
|
||||||
|
"disableVersionDetection": "Disable Version Detection",
|
||||||
|
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
||||||
|
"downloadingX": "Downloading {}",
|
||||||
|
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
||||||
|
"noAPKFound": "No APK found",
|
||||||
|
"noVersionDetection": "No version detection",
|
||||||
|
"categorize": "Categorize",
|
||||||
|
"categories": "Categories",
|
||||||
|
"category": "Category",
|
||||||
|
"noCategory": "No Category",
|
||||||
|
"noCategories": "No Categories",
|
||||||
|
"deleteCategoriesQuestion": "Delete Categories?",
|
||||||
|
"categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.",
|
||||||
|
"addCategory": "Add Category",
|
||||||
|
"label": "Label",
|
||||||
|
"language": "Language",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"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"
|
||||||
|
254
assets/translations/hu.json
Normal file
254
assets/translations/hu.json
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "Érvénytelen a(z) {} app URL-je",
|
||||||
|
"noReleaseFound": "Nem található megfelelő kiadás",
|
||||||
|
"noVersionFound": "Nem sikerült meghatározni a kiadás verzióját",
|
||||||
|
"urlMatchesNoSource": "Az URL nem egyezik ismert forrással",
|
||||||
|
"cantInstallOlderVersion": "Nem telepíthető egy app régebbi verziója",
|
||||||
|
"appIdMismatch": "A letöltött csomagazonosító nem egyezik a meglévő app azonosítóval",
|
||||||
|
"functionNotImplemented": "Ez az osztály nem valósította meg ezt a függvényt",
|
||||||
|
"placeholder": "Helykitöltő",
|
||||||
|
"someErrors": "Néhány hiba történt",
|
||||||
|
"unexpectedError": "Váratlan hiba",
|
||||||
|
"ok": "Oké",
|
||||||
|
"and": "és",
|
||||||
|
"startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva",
|
||||||
|
"bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}",
|
||||||
|
"startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése",
|
||||||
|
"bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött",
|
||||||
|
"firstRun": "Ez az Obtainium első futása",
|
||||||
|
"settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}",
|
||||||
|
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||||
|
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||||
|
"githubPATFormat": "felhasználónév:token",
|
||||||
|
"githubPATLinkText": "A GitHub PAT-okról",
|
||||||
|
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
||||||
|
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
||||||
|
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
||||||
|
"invalidRegEx": "Érvénytelen reguláris kifejezés",
|
||||||
|
"noDescription": "Nincs leírás",
|
||||||
|
"cancel": "Mégse",
|
||||||
|
"continue": "Tovább",
|
||||||
|
"requiredInBrackets": "(Kötelező)",
|
||||||
|
"dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI",
|
||||||
|
"colour": "Szín",
|
||||||
|
"githubStarredRepos": "GitHub Csillagos Repo-k",
|
||||||
|
"uname": "Felh.név",
|
||||||
|
"wrongArgNum": "Rossz számú argumentumot adott meg",
|
||||||
|
"xIsTrackOnly": "A(z) {} csak nyomkövethető",
|
||||||
|
"source": "Forrás",
|
||||||
|
"app": "App",
|
||||||
|
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
|
||||||
|
"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.",
|
||||||
|
"cancelled": "Törölve",
|
||||||
|
"appAlreadyAdded": "Az app már hozzáadva",
|
||||||
|
"alreadyUpToDateQuestion": "Az app már naprakész?",
|
||||||
|
"addApp": "App hozzáadás",
|
||||||
|
"appSourceURL": "App forrás URL",
|
||||||
|
"error": "Hiba",
|
||||||
|
"add": "Hozzáadás",
|
||||||
|
"searchSomeSourcesLabel": "Keresés (csak egyes források)",
|
||||||
|
"search": "Keresés",
|
||||||
|
"additionalOptsFor": "További lehetőségek a következőhöz: {}",
|
||||||
|
"supportedSourcesBelow": "Támogatott források:",
|
||||||
|
"trackOnlyInBrackets": "(Csak nyomonkövetés)",
|
||||||
|
"searchableInBrackets": "(Kereshető)",
|
||||||
|
"appsString": "Appok",
|
||||||
|
"noApps": "Nincs App",
|
||||||
|
"noAppsForFilter": "Nincsenek appok a szűrőhöz",
|
||||||
|
"byX": "{} által",
|
||||||
|
"percentProgress": "Folyamat: {}%",
|
||||||
|
"pleaseWait": "Kis türelmet",
|
||||||
|
"updateAvailable": "Frissítés elérhető",
|
||||||
|
"estimateInBracketsShort": "(Becsült)",
|
||||||
|
"notInstalled": "Nem telepített",
|
||||||
|
"estimateInBrackets": "(Becslés)",
|
||||||
|
"selectAll": "Mindet kiválaszt",
|
||||||
|
"deselectN": "Törölje {} kijelölését",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
|
||||||
|
"removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
|
||||||
|
"removeSelectedApps": "Távolítsa el a kiválasztott appokat",
|
||||||
|
"updateX": "Frissítés: {}",
|
||||||
|
"installX": "Telepítés: {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nas Frissítve",
|
||||||
|
"changeX": "Változás {}",
|
||||||
|
"installUpdateApps": "Appok telepítése/frissítése",
|
||||||
|
"installUpdateSelectedApps": "Telepítse/frissítse a kiválasztott appokat",
|
||||||
|
"onlyWorksWithNonEVDApps": "Csak azoknál az alkalmazásoknál működik, amelyek telepítési állapota nem észlelhető automatikusan (nem gyakori).",
|
||||||
|
"markXSelectedAppsAsUpdated": "Megjelöl {} kiválasztott alkalmazást frissítettként?",
|
||||||
|
"no": "Nem",
|
||||||
|
"yes": "Igen",
|
||||||
|
"markSelectedAppsUpdated": "Jelölje meg a kiválasztott appokat frissítettként",
|
||||||
|
"pinToTop": "Rögzítés a felülre",
|
||||||
|
"unpinFromTop": "Eltávolít felülről",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "Visszaállítja a kiválasztott appok telepítési állapotát?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "A kiválasztott appok telepítési állapota visszaáll.\n\nEz akkor segíthet, ha az Obtainiumban megjelenített app verzió hibás, frissítések vagy egyéb problémák miatt.",
|
||||||
|
"shareSelectedAppURLs": "Ossza meg a kiválasztott app URL címeit",
|
||||||
|
"resetInstallStatus": "Telepítési állapot visszaállítása",
|
||||||
|
"more": "További",
|
||||||
|
"removeOutdatedFilter": "Távolítsa el az elavult alkalmazásszűrőt",
|
||||||
|
"showOutdatedOnly": "Csak az elavult alkalmazások megjelenítése",
|
||||||
|
"filter": "Szűrő",
|
||||||
|
"filterActive": "Szűrő *",
|
||||||
|
"filterApps": "Appok szűrése",
|
||||||
|
"appName": "App név",
|
||||||
|
"author": "Szerző",
|
||||||
|
"upToDateApps": "Naprakész appok",
|
||||||
|
"nonInstalledApps": "Nem telepített appok",
|
||||||
|
"importExport": "Import/Export",
|
||||||
|
"settings": "Beállítások",
|
||||||
|
"exportedTo": "Exportálva ide {}",
|
||||||
|
"obtainiumExport": "Obtainium Export",
|
||||||
|
"invalidInput": "Hibás bemenet",
|
||||||
|
"importedX": "Importálva innen {}",
|
||||||
|
"obtainiumImport": "Obtainium Import",
|
||||||
|
"importFromURLList": "Importálás URL listából",
|
||||||
|
"searchQuery": "Keresési lekérdezés",
|
||||||
|
"appURLList": "App URL lista",
|
||||||
|
"line": "Sor",
|
||||||
|
"searchX": "Keresés {}",
|
||||||
|
"noResults": "Nincs találat",
|
||||||
|
"importX": "Import {}",
|
||||||
|
"importedAppsIdDisclaimer": "Előfordulhat, hogy az importált appok helytelenül \"Nincs telepítve\" jelzéssel jelennek meg.\nA probléma megoldásához telepítse újra őket az Obtainiumon keresztül.\nEz nem érinti az alkalmazásadatokat.\n\nCsak az URL-ekre és a harmadik féltől származó importálási módszerekre vonatkozik..",
|
||||||
|
"importErrors": "Importálási hibák",
|
||||||
|
"importedXOfYApps": "{}/{} app importálva.",
|
||||||
|
"followingURLsHadErrors": "A következő URL-ek hibákat tartalmaztak:",
|
||||||
|
"okay": "Oké",
|
||||||
|
"selectURL": "Válassza ki az URL-t",
|
||||||
|
"selectURLs": "Kiválasztott URL-ek",
|
||||||
|
"pick": "Válasszon",
|
||||||
|
"theme": "Téma",
|
||||||
|
"dark": "Sötét",
|
||||||
|
"light": "Világos",
|
||||||
|
"followSystem": "Rendszer szerint",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"appSortBy": "App rendezés...",
|
||||||
|
"authorName": "Szerző/Név",
|
||||||
|
"nameAuthor": "Név/Szerző",
|
||||||
|
"asAdded": "Mint hozzáadott",
|
||||||
|
"appSortOrder": "Appok rendezése",
|
||||||
|
"ascending": "Emelkedő",
|
||||||
|
"descending": "Csökkenő",
|
||||||
|
"bgUpdateCheckInterval": "Háttérfrissítés ellenőrzési időköz",
|
||||||
|
"neverManualOnly": "Soha – csak manuális",
|
||||||
|
"appearance": "Megjelenés",
|
||||||
|
"showWebInAppView": "Forrás megjelenítése az Appok nézetben",
|
||||||
|
"pinUpdates": "Frissítések kitűzése az App nézet tetejére",
|
||||||
|
"updates": "Frissítések",
|
||||||
|
"sourceSpecific": "Forrás-specifikus",
|
||||||
|
"appSource": "App forrás",
|
||||||
|
"noLogs": "Nincsenek naplók",
|
||||||
|
"appLogs": "App naplók",
|
||||||
|
"close": "Bezár",
|
||||||
|
"share": "Megoszt",
|
||||||
|
"appNotFound": "App nem található",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
|
"pickAnAPK": "Válasszon egy APK-t",
|
||||||
|
"appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:",
|
||||||
|
"deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.",
|
||||||
|
"deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:",
|
||||||
|
"warning": "Figyelem",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "Az alkalmazás forrása „{}”, de a kiadási csomag innen származik: „{}”. Folytatja?",
|
||||||
|
"updatesAvailable": "Frissítések érhetők el",
|
||||||
|
"updatesAvailableNotifDescription": "Értesíti a felhasználót, hogy frissítések állnak rendelkezésre egy vagy több, az Obtainium által nyomon követett alkalmazáshoz",
|
||||||
|
"noNewUpdates": "Nincsenek új frissítések.",
|
||||||
|
"xHasAnUpdate": "A(z) {} frissítést kapott.",
|
||||||
|
"appsUpdated": "Alkalmazások frissítve",
|
||||||
|
"appsUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több app frissítése történt a háttérben",
|
||||||
|
"xWasUpdatedToY": "{} frissítve a következőre: {}.",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"xWasRemovedDueToErrorY": "A(z) {} a következő hiba miatt lett eltávolítva: {}",
|
||||||
|
"completeAppInstallation": "Teljes alkalmazástelepítés",
|
||||||
|
"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",
|
||||||
|
"checkingForUpdates": "Frissítések keresése",
|
||||||
|
"checkingForUpdatesNotifDescription": "Átmeneti értesítés, amely a frissítések keresésekor jelenik meg",
|
||||||
|
"pleaseAllowInstallPerm": "Kérjük, engedélyezze az Obtainiumnak az alkalmazások telepítését",
|
||||||
|
"trackOnly": "Csak követés",
|
||||||
|
"errorWithHttpStatusCode": "Hiba {}",
|
||||||
|
"versionCorrectionDisabled": "Verzió korrekció letiltva (úgy tűnik, a beépülő modul nem működik)",
|
||||||
|
"unknown": "Ismeretlen",
|
||||||
|
"none": "Egyik sem",
|
||||||
|
"never": "Soha",
|
||||||
|
"latestVersionX": "Legújabb verzió: {}",
|
||||||
|
"installedVersionX": "Telepített verzió: {}",
|
||||||
|
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
||||||
|
"remove": "Eltávolítás",
|
||||||
|
"removeAppQuestion": "Eltávolítja az alkalmazást?",
|
||||||
|
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||||
|
"fdroid": "F-Droid",
|
||||||
|
"appIdOrName": "App ID vagy név",
|
||||||
|
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
||||||
|
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
||||||
|
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"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",
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva",
|
||||||
|
"other": "Túl sok kérés (korlátozott arány) – próbálja újra {} perc múlva"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "A háttérfrissítések ellenőrzése {}-t észlelt, {} perc múlva ütemezi az újrapróbálkozást",
|
||||||
|
"other": "A háttérfrissítések ellenőrzése {}-t észlelt, {} perc múlva ütemezi az újrapróbálkozást"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "A háttérfrissítés ellenőrzése {} frissítést talált – szükség esetén értesíti a felhasználót",
|
||||||
|
"other": "A háttérfrissítés ellenőrzése {} frissítést talált – szükség esetén értesíti a felhasználót"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} app",
|
||||||
|
"other": "{} app"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} URL",
|
||||||
|
"other": "{} URL"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} perc",
|
||||||
|
"other": "{} perc"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} óra",
|
||||||
|
"other": "{} óra"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} nap",
|
||||||
|
"other": "{} nap"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "{n} napló törölve (előtte = {előtte}, utána = {utána})",
|
||||||
|
"other": "{n} napló törölve (előtte = {előtte}, utána = {utána})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "A(z) {} és 1 további alkalmazás frissítéseket kapott.",
|
||||||
|
"other": "{} és további {} alkalmazás frissítéseket kapott."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "A(z) {} és 1 további alkalmazás frissítve.",
|
||||||
|
"other": "{} és további {} alkalmazás frissítve."
|
||||||
|
}
|
||||||
|
}
|
@ -16,43 +16,43 @@
|
|||||||
"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:",
|
||||||
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
||||||
"searchableInBrackets": "(Ricercabile)",
|
"searchableInBrackets": "(ricercabile)",
|
||||||
"appsString": "App",
|
"appsString": "App",
|
||||||
"noApps": "Nessuna App",
|
"noApps": "Nessuna App",
|
||||||
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
||||||
@ -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,24 +178,48 @@
|
|||||||
"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": "App ID or Name",
|
"appIdOrName": "ID o nome dell'App",
|
||||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
||||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
"reposHaveMultipleApps": "I repository possono contenere più App",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"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",
|
||||||
"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",
|
||||||
|
256
assets/translations/ja.json
Normal file
256
assets/translations/ja.json
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "{}は有効なソースURLではありません",
|
||||||
|
"noReleaseFound": "適切なリリースが見つかりませんでした",
|
||||||
|
"noVersionFound": "リリースバージョンを特定できませんでした",
|
||||||
|
"urlMatchesNoSource": "URLが既知のソースと一致しません",
|
||||||
|
"cantInstallOlderVersion": "旧バージョンのアプリをインストールできません",
|
||||||
|
"appIdMismatch": "ダウンロードしたパッケージのIDが既存のApp IDと一致しません",
|
||||||
|
"functionNotImplemented": "このクラスはこの機能を実装していません",
|
||||||
|
"placeholder": "プレースホルダー",
|
||||||
|
"someErrors": "いくつかのエラーが発生しました",
|
||||||
|
"unexpectedError": "予期せぬエラーが発生しました",
|
||||||
|
"ok": "OK",
|
||||||
|
"and": "と",
|
||||||
|
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
||||||
|
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||||
|
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
||||||
|
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
||||||
|
"firstRun": "これがObtainiumの最初の実行です",
|
||||||
|
"settingUpdateCheckIntervalTo": "確認間隔を{}に設定する",
|
||||||
|
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||||
|
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||||
|
"githubPATFormat": "ユーザー名:トークン",
|
||||||
|
"githubPATLinkText": "GitHub PATsについて",
|
||||||
|
"includePrereleases": "プレリリースを含む",
|
||||||
|
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||||
|
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||||
|
"invalidRegEx": "無効な正規表現",
|
||||||
|
"noDescription": "説明はありません",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"continue": "続行",
|
||||||
|
"requiredInBrackets": "(必須)",
|
||||||
|
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
|
||||||
|
"colour": "カラー",
|
||||||
|
"githubStarredRepos": "Githubでスターしたリポジトリ",
|
||||||
|
"uname": "ユーザー名",
|
||||||
|
"wrongArgNum": "提供する引数の数が間違っています",
|
||||||
|
"xIsTrackOnly": "{} は「追跡のみ」です",
|
||||||
|
"source": "ソース",
|
||||||
|
"app": "アプリ",
|
||||||
|
"appsFromSourceAreTrackOnly": "このソースからのアプリは「追跡のみ」です。",
|
||||||
|
"youPickedTrackOnly": "「追跡のみ」を選択しています",
|
||||||
|
"trackOnlyAppDescription": "アプリのアップデートは追跡されますが、Obtainiumはアプリのダウンロードやインストールをすることはできません。",
|
||||||
|
"cancelled": "キャンセルしました",
|
||||||
|
"appAlreadyAdded": "アプリはすでに追加されています",
|
||||||
|
"alreadyUpToDateQuestion": "アプリはすでに最新ですか?",
|
||||||
|
"addApp": "アプリの追加",
|
||||||
|
"appSourceURL": "アプリのソースURL",
|
||||||
|
"error": "エラー",
|
||||||
|
"add": "追加",
|
||||||
|
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
|
||||||
|
"search": "検索",
|
||||||
|
"additionalOptsFor": "{}の追加オプション",
|
||||||
|
"supportedSourcesBelow": "対応するソース:",
|
||||||
|
"trackOnlyInBrackets": "(追跡のみ)",
|
||||||
|
"searchableInBrackets": "(検索可能)",
|
||||||
|
"appsString": "アプリ",
|
||||||
|
"noApps": "アプリはありません",
|
||||||
|
"noAppsForFilter": "フィルターに一致するアプリはありません",
|
||||||
|
"byX": "by {}",
|
||||||
|
"percentProgress": "ダウンロード中: {}%",
|
||||||
|
"pleaseWait": "しばらくお待ちください",
|
||||||
|
"updateAvailable": "アップデートが利用可能",
|
||||||
|
"estimateInBracketsShort": "(推定)",
|
||||||
|
"notInstalled": "未インストール",
|
||||||
|
"estimateInBrackets": "(推定)",
|
||||||
|
"selectAll": "すべて選択",
|
||||||
|
"deselectN": "{}件の選択を解除",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。",
|
||||||
|
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
|
||||||
|
"removeSelectedApps": "選択したアプリを削除する",
|
||||||
|
"updateX": "{}をアップデートする",
|
||||||
|
"installX": "{}をインストールする",
|
||||||
|
"markXTrackOnlyAsUpdated": "{}\n(追跡のみ)\nをアップデート済みとしてマークする",
|
||||||
|
"changeX": "{}を変更する",
|
||||||
|
"installUpdateApps": "アプリのインストール/アップデート",
|
||||||
|
"installUpdateSelectedApps": "選択したアプリのインストール/アップデート",
|
||||||
|
"onlyWorksWithNonEVDApps": "インストール状況を自動検出できないアプリ(一般的でないもの)のみ動作します。",
|
||||||
|
"markXSelectedAppsAsUpdated": "{}個の選択したアプリをアップデート済みとしてマークしますか?",
|
||||||
|
"no": "いいえ",
|
||||||
|
"yes": "はい",
|
||||||
|
"markSelectedAppsUpdated": "選択したアプリをアップデート済みとしてマークする",
|
||||||
|
"pinToTop": "トップに固定",
|
||||||
|
"unpinFromTop": "トップから固定解除",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "選択したアプリのインストール状態をリセットしますか?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "選択したアプリのインストール状態がリセットされます。\n\nアップデートに失敗するなどして、Obtainiumに表示されるアプリのバージョンが正しくない場合に役立ちます。",
|
||||||
|
"shareSelectedAppURLs": "選択したアプリのURLを共有する",
|
||||||
|
"resetInstallStatus": "インストール状態をリセットする",
|
||||||
|
"more": "もっと見る",
|
||||||
|
"removeOutdatedFilter": "アップデートが存在するアプリのフィルターを解除",
|
||||||
|
"showOutdatedOnly": "アップデートが存在するアプリのみ表示する",
|
||||||
|
"filter": "フィルター",
|
||||||
|
"filterActive": "フィルター *",
|
||||||
|
"filterApps": "アプリを絞り込む",
|
||||||
|
"appName": "アプリ名",
|
||||||
|
"author": "作者",
|
||||||
|
"upToDateApps": "最新のアプリ",
|
||||||
|
"nonInstalledApps": "未インストールのアプリ",
|
||||||
|
"importExport": "インポート/エクスポート",
|
||||||
|
"settings": "設定",
|
||||||
|
"exportedTo": "{}にエクスポートしました",
|
||||||
|
"obtainiumExport": "Obtainium エクスポート",
|
||||||
|
"invalidInput": "無効な入力",
|
||||||
|
"importedX": "{}をインポートしました",
|
||||||
|
"obtainiumImport": "Obtainium インポート",
|
||||||
|
"importFromURLList": "URLリストからのインポート",
|
||||||
|
"searchQuery": "検索キーワード",
|
||||||
|
"appURLList": "アプリのURLリスト",
|
||||||
|
"line": "行",
|
||||||
|
"searchX": "{}で検索",
|
||||||
|
"noResults": "結果は見つかりませんでした",
|
||||||
|
"importX": "{}をインポートする",
|
||||||
|
"importedAppsIdDisclaimer": "インポートしたアプリが「未インストール」と表示されることがあります。\nこれを解決するには、Obtainiumから再インストールしてください。\nアプリのデータには影響しません。\n\nURLとサードパーティーのインポートメソッドにのみ影響します。",
|
||||||
|
"importErrors": "インポートエラー",
|
||||||
|
"importedXOfYApps": "{} / {} アプリをインポートしました",
|
||||||
|
"followingURLsHadErrors": "以下のURLでエラーが発生しました:",
|
||||||
|
"okay": "OK",
|
||||||
|
"selectURL": "URLを選択",
|
||||||
|
"selectURLs": "URLを選択",
|
||||||
|
"pick": "選択",
|
||||||
|
"theme": "テーマ",
|
||||||
|
"dark": "ダーク",
|
||||||
|
"light": "ライト",
|
||||||
|
"followSystem": "システムに従う",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"appSortBy": "アプリの並び方",
|
||||||
|
"authorName": "作者名/アプリ名",
|
||||||
|
"nameAuthor": "アプリ名/作者名",
|
||||||
|
"asAdded": "追加順",
|
||||||
|
"appSortOrder": "並び順",
|
||||||
|
"ascending": "昇順",
|
||||||
|
"descending": "降順",
|
||||||
|
"bgUpdateCheckInterval": "バックグラウンドでのアップデート確認の間隔",
|
||||||
|
"neverManualOnly": "手動",
|
||||||
|
"appearance": "外観",
|
||||||
|
"showWebInAppView": "アプリビューにソースウェブページを表示する",
|
||||||
|
"pinUpdates": "アップデートがあるアプリをトップに固定する",
|
||||||
|
"updates": "アップデート",
|
||||||
|
"sourceSpecific": "Github アクセストークン",
|
||||||
|
"appSource": "アプリのソース",
|
||||||
|
"noLogs": "ログはありません",
|
||||||
|
"appLogs": "アプリのログ",
|
||||||
|
"close": "閉じる",
|
||||||
|
"share": "共有",
|
||||||
|
"appNotFound": "アプリが見つかりません",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-エクスポート",
|
||||||
|
"pickAnAPK": "APKを選択",
|
||||||
|
"appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ",
|
||||||
|
"deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。",
|
||||||
|
"deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:",
|
||||||
|
"warning": "警告",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
|
||||||
|
"updatesAvailable": "アップデートが利用可能",
|
||||||
|
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
||||||
|
"noNewUpdates": "新しいアップデートはありません",
|
||||||
|
"xHasAnUpdate": "{}のアップデートが利用可能です",
|
||||||
|
"appsUpdated": "アプリをアップデートしました",
|
||||||
|
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
||||||
|
"xWasUpdatedToY": "{}が{}にアップデートされました",
|
||||||
|
"errorCheckingUpdates": "アップデート確認中のエラー",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
|
||||||
|
"appsRemoved": "削除されたアプリ",
|
||||||
|
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
|
||||||
|
"xWasRemovedDueToErrorY": "このエラーのため、{}は削除されました: {}",
|
||||||
|
"completeAppInstallation": "アプリのインストールを完了する",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開いている必要があります。",
|
||||||
|
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります。",
|
||||||
|
"checkingForUpdates": "アップデートを確認中",
|
||||||
|
"checkingForUpdatesNotifDescription": "アップデートを確認する際に表示される一時的な通知",
|
||||||
|
"pleaseAllowInstallPerm": "Obtainiumによるアプリのインストールを許可してください。",
|
||||||
|
"trackOnly": "追跡のみ",
|
||||||
|
"errorWithHttpStatusCode": "エラー {}",
|
||||||
|
"versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)",
|
||||||
|
"unknown": "不明",
|
||||||
|
"none": "なし",
|
||||||
|
"never": "しない",
|
||||||
|
"latestVersionX": "最新のバージョン: {}",
|
||||||
|
"installedVersionX": "インストールされたバージョン: {}",
|
||||||
|
"lastUpdateCheckX": "最終アップデート確認: {}",
|
||||||
|
"remove": "削除",
|
||||||
|
"removeAppQuestion": "アプリを削除しますか?",
|
||||||
|
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||||
|
"fdroid": "F-Droid",
|
||||||
|
"appIdOrName": "アプリのIDまたは名前",
|
||||||
|
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||||
|
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||||
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"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": "言語",
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||||
|
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します",
|
||||||
|
"other": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します",
|
||||||
|
"other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{}個のアプリ",
|
||||||
|
"other": "{}個のアプリ"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{}個のURL",
|
||||||
|
"other": "{}個のURL"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{}分",
|
||||||
|
"other": "{}分"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{}時間",
|
||||||
|
"other": "{}時間"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{}日",
|
||||||
|
"other": "{}日"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "{n}個のログをクリアしました (前 = {before}, 後 = {after})",
|
||||||
|
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{}とさらに{}個のアプリのアップデートが利用可能です",
|
||||||
|
"other": "{}とさらに{}個のアプリのアップデートが利用可能です"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{}とさらに{}個のアプリがアップデートされました",
|
||||||
|
"other": "{}とさらに{}個のアプリがアップデートされました"
|
||||||
|
}
|
||||||
|
}
|
@ -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": "继续",
|
||||||
@ -39,10 +39,10 @@
|
|||||||
"app": "应用程序",
|
"app": "应用程序",
|
||||||
"appsFromSourceAreTrackOnly": "来自此来源的应用为仅追踪",
|
"appsFromSourceAreTrackOnly": "来自此来源的应用为仅追踪",
|
||||||
"youPickedTrackOnly": "你已选择仅追踪选项",
|
"youPickedTrackOnly": "你已选择仅追踪选项",
|
||||||
"trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.",
|
"trackOnlyAppDescription": "该应用程序将被跟踪更新,但 Obtainium 无法下载或安装它",
|
||||||
"cancelled": "已取消",
|
"cancelled": "已取消",
|
||||||
"appAlreadyAdded": "此应用程序已被添加",
|
"appAlreadyAdded": "此应用程序已被添加",
|
||||||
"alreadyUpToDateQuestion": "App Already up to Date?",
|
"alreadyUpToDateQuestion": "应用已是最新?",
|
||||||
"addApp": "添加应用",
|
"addApp": "添加应用",
|
||||||
"appSourceURL": "应用来源 URL",
|
"appSourceURL": "应用来源 URL",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
@ -109,7 +109,7 @@
|
|||||||
"searchX": "搜索 {}",
|
"searchX": "搜索 {}",
|
||||||
"noResults": "无结果",
|
"noResults": "无结果",
|
||||||
"importX": "导入 {}",
|
"importX": "导入 {}",
|
||||||
"importedAppsIdDisclaimer": "导入的应用程序可能不正确地显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
|
"importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
|
||||||
"importErrors": "导入错误",
|
"importErrors": "导入错误",
|
||||||
"importedXOfYApps": "{} 中的 {} 个应用已导入",
|
"importedXOfYApps": "{} 中的 {} 个应用已导入",
|
||||||
"followingURLsHadErrors": "以下 URL 有错误:",
|
"followingURLsHadErrors": "以下 URL 有错误:",
|
||||||
@ -123,21 +123,21 @@
|
|||||||
"followSystem": "跟随系统",
|
"followSystem": "跟随系统",
|
||||||
"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": "指定源",
|
"sourceSpecific": "Github 访问令牌",
|
||||||
"appSource": "应用源",
|
"appSource": "源代码",
|
||||||
"noLogs": "无日志",
|
"noLogs": "无日志",
|
||||||
"appLogs": "应用日志",
|
"appLogs": "应用日志",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
@ -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": "更新可用",
|
||||||
@ -170,21 +170,45 @@
|
|||||||
"pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
|
"pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
|
||||||
"trackOnly": "仅追踪",
|
"trackOnly": "仅追踪",
|
||||||
"errorWithHttpStatusCode": "错误 {}",
|
"errorWithHttpStatusCode": "错误 {}",
|
||||||
"versionCorrectionDisabled": "Version correction disabled (plugin doesn't seem to work)",
|
"versionCorrectionDisabled": "禁用版本更正(插件似乎未起作用)",
|
||||||
"unknown": "Unknown",
|
"unknown": "未知",
|
||||||
"none": "None",
|
"none": "无",
|
||||||
"never": "Never",
|
"never": "从不",
|
||||||
"latestVersionX": "Latest Version: {}",
|
"latestVersionX": "最新: {}",
|
||||||
"installedVersionX": "Installed Version: {}",
|
"installedVersionX": "已安装: {}",
|
||||||
"lastUpdateCheckX": "Last Update Check: {}",
|
"lastUpdateCheckX": "最后检查: {}",
|
||||||
"remove": "Remove",
|
"remove": "删除",
|
||||||
"removeAppQuestion": "Remove App?",
|
"removeAppQuestion": "删除应用?",
|
||||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
"yesMarkUpdated": "'是的,标为已更新",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid",
|
||||||
"appIdOrName": "App ID or Name",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
"appWithIdOrNameNotFound": "没有发现具有此 ID 或名称的应用",
|
||||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
"reposHaveMultipleApps": "来源可能包含多个应用",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid 第三方源",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "安装",
|
||||||
|
"markInstalled": "标记为已安装",
|
||||||
|
"update": "更新",
|
||||||
|
"markUpdated": "标记为已更新",
|
||||||
|
"additionalOptions": "附加选项",
|
||||||
|
"disableVersionDetection": "关闭版本检测",
|
||||||
|
"noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
|
||||||
|
"downloadingX": "下载中 {}",
|
||||||
|
"downloadNotifDescription": "通知用户下载进度",
|
||||||
|
"noAPKFound": "未找到安装包",
|
||||||
|
"noVersionDetection": "无版本检测",
|
||||||
|
"categorize": "Categorize",
|
||||||
|
"categories": "Categories",
|
||||||
|
"category": "Category",
|
||||||
|
"noCategory": "No Category",
|
||||||
|
"noCategories": "No Categories",
|
||||||
|
"deleteCategoriesQuestion": "Delete Categories?",
|
||||||
|
"categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.",
|
||||||
|
"addCategory": "Add Category",
|
||||||
|
"label": "Label",
|
||||||
|
"language": "Language",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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')),
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,12 +12,9 @@ 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',
|
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
@ -52,19 +49,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 +95,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,14 +105,17 @@ 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?)
|
||||||
|
?.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: 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'));
|
||||||
@ -148,7 +147,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];
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
lib/app_sources/steammobile.dart
Normal file
60
lib/app_sources/steammobile.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class SteamMobile extends AppSource {
|
||||||
|
SteamMobile() {
|
||||||
|
host = 'store.steampowered.com';
|
||||||
|
name = tr('steam');
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[GeneratedFormDropdown('app', apks.entries.toList(), label: tr('app'))]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
return 'https://$host';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Response res = await get(Uri.parse('https://$host/mobile'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var apkNamePrefix = additionalSettings['app'] as String?;
|
||||||
|
if (apkNamePrefix == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
String apkInURLRegexPattern = '/$apkNamePrefix-[^/]+\\.apk\$';
|
||||||
|
var links = parse(res.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
|
.where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (links.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
var versionMatch = RegExp(apkInURLRegexPattern).firstMatch(links[0]);
|
||||||
|
if (versionMatch == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
var version = links[0].substring(
|
||||||
|
versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4);
|
||||||
|
var apkUrls = [links[0]];
|
||||||
|
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,50 +1,127 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
|
|
||||||
enum FormItemType { string, bool }
|
abstract class GeneratedFormItem {
|
||||||
|
|
||||||
typedef OnValueChanges = void Function(
|
|
||||||
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<String>? opts;
|
|
||||||
|
|
||||||
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.hint})
|
||||||
this.id = 'input',
|
: super(key,
|
||||||
this.belowWidgets = const [],
|
label: label,
|
||||||
this.hint,
|
belowWidgets: belowWidgets,
|
||||||
this.opts,
|
defaultValue: defaultValue,
|
||||||
this.key = 'default'}) {
|
additionalValidators: additionalValidators);
|
||||||
if (type != FormItemType.string) {
|
|
||||||
required = false;
|
@override
|
||||||
|
String ensureType(val) {
|
||||||
|
return val.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GeneratedFormDropdown extends GeneratedFormItem {
|
||||||
|
late List<MapEntry<String, String>>? opts;
|
||||||
|
|
||||||
|
GeneratedFormDropdown(
|
||||||
|
String key,
|
||||||
|
this.opts, {
|
||||||
|
String label = 'Input',
|
||||||
|
List<Widget> belowWidgets = const [],
|
||||||
|
String defaultValue = '',
|
||||||
|
List<String? Function(String? value)> additionalValidators = const [],
|
||||||
|
}) : super(key,
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
additionalValidators: additionalValidators);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String ensureType(val) {
|
||||||
|
return val.toString();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneratedFormSwitch extends GeneratedFormItem {
|
||||||
|
GeneratedFormSwitch(
|
||||||
|
String key, {
|
||||||
|
String label = 'Input',
|
||||||
|
List<Widget> belowWidgets = const [],
|
||||||
|
bool defaultValue = false,
|
||||||
|
List<String? Function(bool value)> additionalValidators = const [],
|
||||||
|
}) : super(key,
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
additionalValidators: additionalValidators);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool ensureType(val) {
|
||||||
|
return val == true || val == 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneratedFormTagInput extends GeneratedFormItem {
|
||||||
|
late MapEntry<String, String>? deleteConfirmationMessage;
|
||||||
|
late bool singleSelect;
|
||||||
|
late WrapAlignment alignment;
|
||||||
|
late String emptyMessage;
|
||||||
|
GeneratedFormTagInput(String key,
|
||||||
|
{String label = 'Input',
|
||||||
|
List<Widget> belowWidgets = const [],
|
||||||
|
Map<String, MapEntry<int, bool>> defaultValue = const {},
|
||||||
|
List<String? Function(Map<String, MapEntry<int, bool>> value)>
|
||||||
|
additionalValidators = const [],
|
||||||
|
this.deleteConfirmationMessage,
|
||||||
|
this.singleSelect = false,
|
||||||
|
this.alignment = WrapAlignment.start,
|
||||||
|
this.emptyMessage = 'Input'})
|
||||||
|
: super(key,
|
||||||
|
label: label,
|
||||||
|
belowWidgets: belowWidgets,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
additionalValidators: additionalValidators);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, MapEntry<int, bool>> ensureType(val) {
|
||||||
|
return val is Map<String, MapEntry<int, bool>> ? val : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef OnValueChanges = void Function(
|
||||||
|
Map<String, dynamic> values, bool valid, bool isBuilding);
|
||||||
|
|
||||||
class GeneratedForm extends StatefulWidget {
|
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();
|
||||||
@ -52,17 +129,16 @@ class GeneratedForm extends StatefulWidget {
|
|||||||
|
|
||||||
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 &&
|
valid = valid &&
|
||||||
((formInputs[r][i].key as GlobalKey<FormFieldState>)
|
((formInputs[r][i].key as GlobalKey<FormFieldState>)
|
||||||
@ -75,47 +151,61 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
widget.onValueChanges(returnValues, valid, isBuilding);
|
widget.onValueChanges(returnValues, valid, isBuilding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates a random light color
|
||||||
|
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||||
|
Color generateRandomLightColor() {
|
||||||
|
// Create a random number generator
|
||||||
|
final Random random = Random();
|
||||||
|
|
||||||
|
// Generate random hue, saturation, and value values
|
||||||
|
final double hue = random.nextDouble() * 360;
|
||||||
|
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||||
|
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||||
|
|
||||||
|
// Create a HSV color with the random values
|
||||||
|
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// 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
|
|
||||||
: '';
|
|
||||||
}).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(
|
||||||
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,20 +214,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: tr('colour')),
|
decoration: InputDecoration(labelText: formItem.label),
|
||||||
value: values[row.key][e.key],
|
value: values[formItem.key],
|
||||||
items: e.value.opts!
|
items: formItem.opts!
|
||||||
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
.map((e2) =>
|
||||||
|
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;
|
values[formItem.key] = value ?? formItem.opts!.first.key;
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -153,21 +243,173 @@ 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] = Wrap(
|
||||||
|
alignment: (widget.items[r][e] as GeneratedFormTagInput).alignment,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.isEmpty ==
|
||||||
|
true
|
||||||
|
? Text(
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.emptyMessage,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
...(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.entries
|
||||||
|
.map((e2) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: ChoiceChip(
|
||||||
|
label: Text(e2.key),
|
||||||
|
backgroundColor: Color(e2.value.key).withAlpha(50),
|
||||||
|
selectedColor: Color(e2.value.key),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
selected: e2.value.value,
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
(values[widget.items[r][e].key] as Map<String,
|
||||||
|
MapEntry<int, bool>>)[e2.key] =
|
||||||
|
MapEntry(
|
||||||
|
(values[widget.items[r][e].key] as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>>)[e2.key]!
|
||||||
|
.key,
|
||||||
|
value);
|
||||||
|
if ((widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.singleSelect &&
|
||||||
|
value == true) {
|
||||||
|
for (var key in (values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>)
|
||||||
|
.keys) {
|
||||||
|
if (key != e2.key) {
|
||||||
|
(values[widget.items[r][e].key] as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int,
|
||||||
|
bool>>)[key] = MapEntry(
|
||||||
|
(values[widget.items[r][e].key] as Map<
|
||||||
|
String,
|
||||||
|
MapEntry<int, bool>>)[key]!
|
||||||
|
.key,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}) ??
|
||||||
|
[const SizedBox.shrink()],
|
||||||
|
(values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
?.values
|
||||||
|
.where((e) => e.value)
|
||||||
|
.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
fn() {
|
||||||
|
setState(() {
|
||||||
|
var temp = values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>;
|
||||||
|
temp.removeWhere((key, value) => value.value);
|
||||||
|
values[widget.items[r][e].key] = temp;
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.deleteConfirmationMessage !=
|
||||||
|
null) {
|
||||||
|
var message =
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.deleteConfirmationMessage!;
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: message.key,
|
||||||
|
message: message.value,
|
||||||
|
items: const []);
|
||||||
|
}).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('remove'),
|
||||||
|
))
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<Map<String, dynamic>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: widget.items[r][e].label,
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('label',
|
||||||
|
label: tr('label'))
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}).then((value) {
|
||||||
|
String? label = value?['label'];
|
||||||
|
if (label != null) {
|
||||||
|
setState(() {
|
||||||
|
var temp = values[widget.items[r][e].key]
|
||||||
|
as Map<String, MapEntry<int, bool>>?;
|
||||||
|
temp ??= {};
|
||||||
|
var singleSelect =
|
||||||
|
(widget.items[r][e] as GeneratedFormTagInput)
|
||||||
|
.singleSelect;
|
||||||
|
var someSelected = temp.entries
|
||||||
|
.where((element) => element.value.value)
|
||||||
|
.isNotEmpty;
|
||||||
|
temp[label] = MapEntry(
|
||||||
|
generateRandomLightColor().value,
|
||||||
|
!(someSelected && singleSelect));
|
||||||
|
values[widget.items[r][e].key] = temp;
|
||||||
|
someValueChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('add'),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,9 +419,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,
|
||||||
)
|
)
|
||||||
@ -216,18 +457,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;
|
|
||||||
}
|
|
||||||
|
@ -8,14 +8,12 @@ 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 = ''});
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,13 +21,12 @@ class GeneratedFormModal extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,8 +54,7 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
this.valid = valid;
|
this.valid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
defaultValues: widget.defaultValues)
|
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -21,13 +21,20 @@ 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.13';
|
const String currentVersion = '0.9.8';
|
||||||
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
|
||||||
|
|
||||||
const int bgUpdateCheckAlarmId = 666;
|
const int bgUpdateCheckAlarmId = 666;
|
||||||
|
|
||||||
const supportedLocales = [Locale('en'), Locale('zh'), Locale('it')];
|
const supportedLocales = [
|
||||||
|
Locale('en'),
|
||||||
|
Locale('zh'),
|
||||||
|
Locale('it'),
|
||||||
|
Locale('ja'),
|
||||||
|
Locale('hu'),
|
||||||
|
Locale('de')
|
||||||
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
|
|
||||||
@ -36,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) {
|
||||||
@ -87,7 +98,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
if (e is RateLimitError || e is SocketException) {
|
if (e is RateLimitError || e is SocketException) {
|
||||||
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||||
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
||||||
args: [(e as AppSource).name, remainingMinutes.toString()]));
|
args: [e.toString(), remainingMinutes.toString()]));
|
||||||
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||||
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||||
@ -153,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()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -193,9 +205,8 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
currentReleaseTag,
|
currentReleaseTag,
|
||||||
[],
|
[],
|
||||||
0,
|
0,
|
||||||
['true'],
|
{'includePrereleases': true},
|
||||||
null,
|
null,
|
||||||
false,
|
|
||||||
false)
|
false)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,8 @@ 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 = [];
|
|
||||||
bool otherAdditionalDataIsValid = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -43,10 +41,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
var source = valid ? sourceProvider.getSource(userInput) : null;
|
||||||
if (pickedSource.runtimeType != source.runtimeType) {
|
if (pickedSource.runtimeType != source.runtimeType) {
|
||||||
pickedSource = source;
|
pickedSource = source;
|
||||||
sourceSpecificAdditionalData =
|
additionalSettings = source != null
|
||||||
source != null ? source.additionalSourceAppSpecificDefaults : [];
|
? getDefaultValuesFromFormItems(
|
||||||
sourceSpecificDataIsValid = source != null
|
source.combinedAppSpecificSettingFormItems)
|
||||||
? !sourceProvider.ifSourceAppsRequireAdditionalData(source)
|
: {};
|
||||||
|
additionalSettingsValid = source != null
|
||||||
|
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
||||||
: true;
|
: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,11 +66,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 +81,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 +88,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,7 +128,7 @@ 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;
|
||||||
}
|
}
|
||||||
await appsProvider.saveApps([app]);
|
await appsProvider.saveApps([app]);
|
||||||
@ -157,7 +169,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
child: GeneratedForm(
|
child: GeneratedForm(
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormTextField('appSourceURL',
|
||||||
label: tr('appSourceURL'),
|
label: tr('appSourceURL'),
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
@ -180,10 +192,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
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 +204,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,7 +231,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
child: GeneratedForm(
|
child: GeneratedForm(
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormTextField(
|
||||||
|
'searchSomeSources',
|
||||||
label: tr('searchSomeSourcesLabel'),
|
label: tr('searchSomeSourcesLabel'),
|
||||||
required: false),
|
required: false),
|
||||||
]
|
]
|
||||||
@ -232,11 +240,11 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (values.isNotEmpty && valid) {
|
if (values.isNotEmpty && valid) {
|
||||||
setState(() {
|
setState(() {
|
||||||
searchQuery = values[0].trim();
|
searchQuery =
|
||||||
|
values['searchSomeSources']!.trim();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
defaultValues: const ['']),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
@ -292,15 +300,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null &&
|
if (pickedSource != null &&
|
||||||
(pickedSource!.additionalSourceAppSpecificDefaults
|
(pickedSource!
|
||||||
.isNotEmpty ||
|
.combinedAppSpecificSettingFormItems.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,52 +317,17 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (pickedSource!
|
|
||||||
.additionalSourceAppSpecificFormItems
|
|
||||||
.isNotEmpty)
|
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
items: pickedSource!
|
items: pickedSource!
|
||||||
.additionalSourceAppSpecificFormItems,
|
.combinedAppSpecificSettingFormItems,
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
if (isBuilding) {
|
if (!isBuilding) {
|
||||||
sourceSpecificAdditionalData = values;
|
|
||||||
sourceSpecificDataIsValid = valid;
|
|
||||||
} else {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
sourceSpecificAdditionalData = values;
|
additionalSettings = values;
|
||||||
sourceSpecificDataIsValid = valid;
|
additionalSettingsValid = valid;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
defaultValues: pickedSource!
|
|
||||||
.additionalSourceAppSpecificDefaults),
|
|
||||||
if (pickedSource!
|
|
||||||
.additionalAppSpecificSourceAgnosticDefaults
|
|
||||||
.isNotEmpty)
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
GeneratedForm(
|
|
||||||
items: pickedSource!
|
|
||||||
.additionalAppSpecificSourceAgnosticFormItems
|
|
||||||
.where((e) => pickedSource!.enforceTrackOnly
|
|
||||||
? e.key != 'trackOnlyFormItemKey'
|
|
||||||
: true)
|
|
||||||
.map((e) => [e])
|
|
||||||
.toList(),
|
|
||||||
onValueChanges: (values, valid, isBuilding) {
|
|
||||||
if (isBuilding) {
|
|
||||||
otherAdditionalData = values;
|
|
||||||
otherAdditionalDataIsValid = valid;
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
otherAdditionalData = values;
|
|
||||||
otherAdditionalDataIsValid = valid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
defaultValues: pickedSource!
|
|
||||||
.additionalAppSpecificSourceAgnosticDefaults),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
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/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';
|
||||||
@ -33,6 +35,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var categories = settingsProvider.categories;
|
||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||||
@ -40,16 +43,33 @@ class _AppPageState extends State<AppPage> {
|
|||||||
prevApp = app;
|
prevApp = app;
|
||||||
getUpdate(app.app.id);
|
getUpdate(app.app.id);
|
||||||
}
|
}
|
||||||
|
var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
|
||||||
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(
|
||||||
|
Theme.of(context).colorScheme.background)
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onWebResourceError: (WebResourceError error) {
|
||||||
|
if (error.isForMainFrame == true) {
|
||||||
|
showError(
|
||||||
|
ObtainiumError(error.description,
|
||||||
|
unexpected: true),
|
||||||
|
context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
..loadRequest(Uri.parse(app.app.url)))
|
||||||
|
: Container()
|
||||||
: CustomScrollView(
|
: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
@ -72,7 +92,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
height: 25,
|
height: 25,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
app?.installedInfo?.name ?? app?.app.name ?? 'App',
|
app?.installedInfo?.name ??
|
||||||
|
app?.app.name ??
|
||||||
|
tr('app'),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
),
|
),
|
||||||
@ -111,7 +133,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
Text(
|
Text(
|
||||||
'${tr('installedVersionX', args: [
|
'${tr('installedVersionX', args: [
|
||||||
app?.app.installedVersion ?? tr('none')
|
app?.app.installedVersion ?? tr('none')
|
||||||
])}${app?.app.trackOnly == true ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||||
tr('app')
|
tr('app')
|
||||||
])}' : ''}',
|
])}' : ''}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -129,7 +151,24 @@ class _AppPageState extends State<AppPage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
)
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
CategoryEditorSelector(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
singleSelect: true,
|
||||||
|
preselected: app?.app.category != null
|
||||||
|
? {app!.app.category!}
|
||||||
|
: {},
|
||||||
|
onSelected: (categories) {
|
||||||
|
if (app != null) {
|
||||||
|
app.app.category = categories.isNotEmpty
|
||||||
|
? categories[0]
|
||||||
|
: null;
|
||||||
|
appsProvider.saveApps([app.app]);
|
||||||
|
}
|
||||||
|
})
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
@ -151,7 +190,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 +238,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,7 +287,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip: 'Additional Options',
|
tooltip: tr('additionalOptions'),
|
||||||
icon: const Icon(Icons.settings)),
|
icon: const Icon(Icons.settings)),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -242,7 +299,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 +323,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
|
||||||
|
@ -80,9 +80,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
!(filter!.includeNonInstalled)) {
|
!(filter!.includeNonInstalled)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) {
|
if (filter!.nameFilter.isNotEmpty || filter!.authorFilter.isNotEmpty) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
List<String> nameTokens = filter!.nameFilter
|
List<String> nameTokens = filter!.nameFilter
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.where((element) => element.trim().isNotEmpty)
|
.where((element) => element.trim().isNotEmpty)
|
||||||
@ -103,6 +101,11 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (filter!.categoryFilter.isNotEmpty &&
|
||||||
|
filter!.categoryFilter != app.app.category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@ -139,14 +142,14 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
|
|
||||||
List<String> trackOnlyUpdateIdsAllOrSelected = [];
|
List<String> trackOnlyUpdateIdsAllOrSelected = [];
|
||||||
existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
|
existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
|
||||||
if (appsProvider.apps[id]!.app.trackOnly) {
|
if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
|
||||||
trackOnlyUpdateIdsAllOrSelected.add(id);
|
trackOnlyUpdateIdsAllOrSelected.add(id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
|
newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
|
||||||
if (appsProvider.apps[id]!.app.trackOnly) {
|
if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
|
||||||
trackOnlyUpdateIdsAllOrSelected.add(id);
|
trackOnlyUpdateIdsAllOrSelected.add(id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -223,7 +226,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
String? changesUrl = SourceProvider()
|
String? changesUrl = SourceProvider()
|
||||||
.getSource(sortedApps[index].app.url)
|
.getSource(sortedApps[index].app.url)
|
||||||
.changeLogPageFromStandardUrl(sortedApps[index].app.url);
|
.changeLogPageFromStandardUrl(sortedApps[index].app.url);
|
||||||
return ListTile(
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.symmetric(
|
||||||
|
vertical: BorderSide(
|
||||||
|
width: 3,
|
||||||
|
color: Color(settingsProvider.categories[
|
||||||
|
sortedApps[index].app.category] ??
|
||||||
|
const Color.fromARGB(0, 0, 0, 0).value)))),
|
||||||
|
child: ListTile(
|
||||||
tileColor: sortedApps[index].app.pinned
|
tileColor: sortedApps[index].app.pinned
|
||||||
? Colors.grey.withOpacity(0.1)
|
? Colors.grey.withOpacity(0.1)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
@ -247,9 +258,11 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: sortedApps[index].app.pinned
|
fontWeight: sortedApps[index].app.pinned
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal),
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
subtitle: Text(tr('byX', args: [sortedApps[index].app.author]),
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
tr('byX', args: [sortedApps[index].app.author]),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: sortedApps[index].app.pinned
|
fontWeight: sortedApps[index].app.pinned
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
@ -271,13 +284,18 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
child: Text(
|
child: Text(
|
||||||
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.trackOnly == true ? ' ${tr('estimateInBrackets')}' : ''}',
|
'${sortedApps[index].app.installedVersion ?? tr('notInstalled')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBrackets')}' : ''}',
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
)),
|
)),
|
||||||
sortedApps[index].app.installedVersion != null &&
|
|
||||||
sortedApps[index].app.installedVersion !=
|
sortedApps[index].app.installedVersion !=
|
||||||
sortedApps[index].app.latestVersion
|
null &&
|
||||||
|
sortedApps[index]
|
||||||
|
.app
|
||||||
|
.installedVersion !=
|
||||||
|
sortedApps[index]
|
||||||
|
.app
|
||||||
|
.latestVersion
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
onTap: changesUrl == null
|
onTap: changesUrl == null
|
||||||
? null
|
? null
|
||||||
@ -286,13 +304,16 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
mode: LaunchMode
|
mode: LaunchMode
|
||||||
.externalApplication);
|
.externalApplication);
|
||||||
},
|
},
|
||||||
child: appsProvider.areDownloadsRunning()
|
child: appsProvider
|
||||||
|
.areDownloadsRunning()
|
||||||
? Text(tr('pleaseWait'))
|
? Text(tr('pleaseWait'))
|
||||||
: Text(
|
: Text(
|
||||||
'${tr('updateAvailable')}${sortedApps[index].app.trackOnly ? ' ${tr('estimateInBracketsShort')}' : ''}',
|
'${tr('updateAvailable')}${sortedApps[index].app.additionalSettings['trackOnly'] == true ? ' ${tr('estimateInBracketsShort')}' : ''}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle:
|
||||||
decoration: changesUrl == null
|
FontStyle.italic,
|
||||||
|
decoration: changesUrl ==
|
||||||
|
null
|
||||||
? TextDecoration.none
|
? TextDecoration.none
|
||||||
: TextDecoration
|
: TextDecoration
|
||||||
.underline),
|
.underline),
|
||||||
@ -312,7 +333,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
));
|
||||||
}, childCount: sortedApps.length))
|
}, childCount: sortedApps.length))
|
||||||
])),
|
])),
|
||||||
persistentFooterButtons: [
|
persistentFooterButtons: [
|
||||||
@ -343,13 +364,12 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: IconButton(
|
: IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog<List<String>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('removeSelectedAppsQuestion'),
|
title: tr('removeSelectedAppsQuestion'),
|
||||||
items: const [],
|
items: const [],
|
||||||
defaultValues: const [],
|
|
||||||
initValid: true,
|
initValid: true,
|
||||||
message: tr(
|
message: tr(
|
||||||
'xWillBeRemovedButRemainInstalled',
|
'xWillBeRemovedButRemainInstalled',
|
||||||
@ -376,41 +396,35 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
List<GeneratedFormItem> formInputs = [];
|
List<GeneratedFormItem> formItems = [];
|
||||||
List<String> defaultValues = [];
|
|
||||||
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
|
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
|
||||||
formInputs.add(GeneratedFormItem(
|
formItems.add(GeneratedFormSwitch('updates',
|
||||||
label: tr('updateX', args: [
|
label: tr('updateX', args: [
|
||||||
plural('apps',
|
plural('apps',
|
||||||
existingUpdateIdsAllOrSelected.length)
|
existingUpdateIdsAllOrSelected.length)
|
||||||
]),
|
]),
|
||||||
type: FormItemType.bool,
|
defaultValue: true));
|
||||||
key: 'updates'));
|
|
||||||
defaultValues.add('true');
|
|
||||||
}
|
}
|
||||||
if (newInstallIdsAllOrSelected.isNotEmpty) {
|
if (newInstallIdsAllOrSelected.isNotEmpty) {
|
||||||
formInputs.add(GeneratedFormItem(
|
formItems.add(GeneratedFormSwitch('installs',
|
||||||
label: tr('installX', args: [
|
label: tr('installX', args: [
|
||||||
plural('apps',
|
plural('apps',
|
||||||
newInstallIdsAllOrSelected.length)
|
newInstallIdsAllOrSelected.length)
|
||||||
]),
|
]),
|
||||||
type: FormItemType.bool,
|
defaultValue: existingUpdateIdsAllOrSelected
|
||||||
key: 'installs'));
|
.isNotEmpty));
|
||||||
defaultValues
|
|
||||||
.add(defaultValues.isEmpty ? 'true' : '');
|
|
||||||
}
|
}
|
||||||
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
||||||
formInputs.add(GeneratedFormItem(
|
formItems.add(GeneratedFormSwitch('trackonlies',
|
||||||
label: tr('markXTrackOnlyAsUpdated', args: [
|
label: tr('markXTrackOnlyAsUpdated', args: [
|
||||||
plural('apps',
|
plural('apps',
|
||||||
trackOnlyUpdateIdsAllOrSelected.length)
|
trackOnlyUpdateIdsAllOrSelected.length)
|
||||||
]),
|
]),
|
||||||
type: FormItemType.bool,
|
defaultValue: existingUpdateIdsAllOrSelected
|
||||||
key: 'trackonlies'));
|
.isNotEmpty ||
|
||||||
defaultValues
|
newInstallIdsAllOrSelected.isNotEmpty));
|
||||||
.add(defaultValues.isEmpty ? 'true' : '');
|
|
||||||
}
|
}
|
||||||
showDialog<List<String>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
var totalApps = existingUpdateIdsAllOrSelected
|
var totalApps = existingUpdateIdsAllOrSelected
|
||||||
@ -420,27 +434,21 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('changeX',
|
title: tr('changeX',
|
||||||
args: [plural('apps', totalApps)]),
|
args: [plural('apps', totalApps)]),
|
||||||
items: formInputs.map((e) => [e]).toList(),
|
items: formItems.map((e) => [e]).toList(),
|
||||||
defaultValues: defaultValues,
|
|
||||||
initValid: true,
|
initValid: true,
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
if (values.isEmpty) {
|
if (values.isEmpty) {
|
||||||
values = defaultValues;
|
values = getDefaultValuesFromFormItems(
|
||||||
|
[formItems]);
|
||||||
}
|
}
|
||||||
bool shouldInstallUpdates =
|
bool shouldInstallUpdates =
|
||||||
findGeneratedFormValueByKey(
|
values['updates'] == true;
|
||||||
formInputs, values, 'updates') ==
|
|
||||||
'true';
|
|
||||||
bool shouldInstallNew =
|
bool shouldInstallNew =
|
||||||
findGeneratedFormValueByKey(
|
values['installs'] == true;
|
||||||
formInputs, values, 'installs') ==
|
|
||||||
'true';
|
|
||||||
bool shouldMarkTrackOnlies =
|
bool shouldMarkTrackOnlies =
|
||||||
findGeneratedFormValueByKey(formInputs,
|
values['trackonlies'] == true;
|
||||||
values, 'trackonlies') ==
|
|
||||||
'true';
|
|
||||||
(() async {
|
(() async {
|
||||||
if (shouldInstallNew ||
|
if (shouldInstallNew ||
|
||||||
shouldInstallUpdates) {
|
shouldInstallUpdates) {
|
||||||
@ -613,7 +621,6 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
title: tr(
|
title: tr(
|
||||||
'resetInstallStatusForSelectedAppsQuestion'),
|
'resetInstallStatusForSelectedAppsQuestion'),
|
||||||
items: const [],
|
items: const [],
|
||||||
defaultValues: const [],
|
|
||||||
initValid: true,
|
initValid: true,
|
||||||
message: tr(
|
message: tr(
|
||||||
'installStatusOfXWillBeResetExplanation',
|
'installStatusOfXWillBeResetExplanation',
|
||||||
@ -683,36 +690,44 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: FontWeight.bold),
|
: FontWeight.bold),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog<List<String>?>(
|
showDialog<Map<String, dynamic>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
|
var vals = filter == null
|
||||||
|
? AppsFilter().toValuesMap()
|
||||||
|
: filter!.toValuesMap();
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('filterApps'),
|
title: tr('filterApps'),
|
||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormTextField('appName',
|
||||||
label: tr('appName'), required: false),
|
label: tr('appName'),
|
||||||
GeneratedFormItem(
|
required: false,
|
||||||
label: tr('author'), required: false)
|
defaultValue: vals['appName']),
|
||||||
|
GeneratedFormTextField('author',
|
||||||
|
label: tr('author'),
|
||||||
|
required: false,
|
||||||
|
defaultValue: vals['author'])
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormSwitch('upToDateApps',
|
||||||
label: tr('upToDateApps'),
|
label: tr('upToDateApps'),
|
||||||
type: FormItemType.bool)
|
defaultValue: vals['upToDateApps'])
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormSwitch('nonInstalledApps',
|
||||||
label: tr('nonInstalledApps'),
|
label: tr('nonInstalledApps'),
|
||||||
type: FormItemType.bool)
|
defaultValue: vals['nonInstalledApps'])
|
||||||
]
|
|
||||||
],
|
],
|
||||||
defaultValues: filter == null
|
[
|
||||||
? AppsFilter().toValuesArray()
|
settingsProvider.getCategoryFormItem(
|
||||||
: filter!.toValuesArray());
|
initCategory: vals['category'] ?? '')
|
||||||
|
]
|
||||||
|
]);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
filter = AppsFilter.fromValuesArray(values);
|
filter = AppsFilter.fromValuesMap(values);
|
||||||
if (AppsFilter().isIdenticalTo(filter!)) {
|
if (AppsFilter().isIdenticalTo(filter!)) {
|
||||||
filter = null;
|
filter = null;
|
||||||
}
|
}
|
||||||
@ -733,32 +748,37 @@ class AppsFilter {
|
|||||||
late String authorFilter;
|
late String authorFilter;
|
||||||
late bool includeUptodate;
|
late bool includeUptodate;
|
||||||
late bool includeNonInstalled;
|
late bool includeNonInstalled;
|
||||||
|
late String categoryFilter;
|
||||||
|
|
||||||
AppsFilter(
|
AppsFilter(
|
||||||
{this.nameFilter = '',
|
{this.nameFilter = '',
|
||||||
this.authorFilter = '',
|
this.authorFilter = '',
|
||||||
this.includeUptodate = true,
|
this.includeUptodate = true,
|
||||||
this.includeNonInstalled = true});
|
this.includeNonInstalled = true,
|
||||||
|
this.categoryFilter = ''});
|
||||||
|
|
||||||
List<String> toValuesArray() {
|
Map<String, dynamic> toValuesMap() {
|
||||||
return [
|
return {
|
||||||
nameFilter,
|
'appName': nameFilter,
|
||||||
authorFilter,
|
'author': authorFilter,
|
||||||
includeUptodate ? 'true' : '',
|
'upToDateApps': includeUptodate,
|
||||||
includeNonInstalled ? 'true' : ''
|
'nonInstalledApps': includeNonInstalled,
|
||||||
];
|
'category': categoryFilter
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AppsFilter.fromValuesArray(List<String> values) {
|
AppsFilter.fromValuesMap(Map<String, dynamic> values) {
|
||||||
nameFilter = values[0];
|
nameFilter = values['appName']!;
|
||||||
authorFilter = values[1];
|
authorFilter = values['author']!;
|
||||||
includeUptodate = values[2] == 'true';
|
includeUptodate = values['upToDateApps'];
|
||||||
includeNonInstalled = values[3] == 'true';
|
includeNonInstalled = values['nonInstalledApps'];
|
||||||
|
categoryFilter = values['category']!;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isIdenticalTo(AppsFilter other) =>
|
bool isIdenticalTo(AppsFilter other) =>
|
||||||
authorFilter.trim() == other.authorFilter.trim() &&
|
authorFilter.trim() == other.authorFilter.trim() &&
|
||||||
nameFilter.trim() == other.nameFilter.trim() &&
|
nameFilter.trim() == other.nameFilter.trim() &&
|
||||||
includeUptodate == other.includeUptodate &&
|
includeUptodate == other.includeUptodate &&
|
||||||
includeNonInstalled == other.includeNonInstalled;
|
includeNonInstalled == other.includeNonInstalled &&
|
||||||
|
categoryFilter.trim() == other.categoryFilter.trim();
|
||||||
}
|
}
|
||||||
|
@ -138,18 +138,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 +173,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 +226,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 +238,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 +338,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 +354,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 +367,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
var urlsWithDescriptions =
|
var urlsWithDescriptions =
|
||||||
await source
|
await source
|
||||||
.getUrlsWithDescriptions(
|
.getUrlsWithDescriptions(
|
||||||
values);
|
values.values
|
||||||
|
.map((e) =>
|
||||||
|
e.toString())
|
||||||
|
.toList());
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<String>?>(
|
List<String>?>(
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/main.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -17,6 +22,21 @@ class SettingsPage extends StatefulWidget {
|
|||||||
State<SettingsPage> createState() => _SettingsPageState();
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates a random light color
|
||||||
|
// Courtesy of ChatGPT 😭 (with a bugfix 🥳)
|
||||||
|
Color generateRandomLightColor() {
|
||||||
|
// Create a random number generator
|
||||||
|
final Random random = Random();
|
||||||
|
|
||||||
|
// Generate random hue, saturation, and value values
|
||||||
|
final double hue = random.nextDouble() * 360;
|
||||||
|
final double saturation = 0.5 + random.nextDouble() * 0.5;
|
||||||
|
final double value = 0.9 + random.nextDouble() * 0.1;
|
||||||
|
|
||||||
|
// Create a HSV color with the random values
|
||||||
|
return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor();
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -110,6 +130,28 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var localeDropdown = DropdownButtonFormField(
|
||||||
|
decoration: InputDecoration(labelText: tr('language')),
|
||||||
|
value: settingsProvider.forcedLocale,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: null,
|
||||||
|
child: Text(tr('followSystem')),
|
||||||
|
),
|
||||||
|
...supportedLocales.map((e) => DropdownMenuItem(
|
||||||
|
value: e.toLanguageTag(),
|
||||||
|
child: Text(e.toLanguageTag().toUpperCase()),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.forcedLocale = value;
|
||||||
|
if (value != null) {
|
||||||
|
context.setLocale(Locale(value));
|
||||||
|
} else {
|
||||||
|
context.resetLocale();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var intervalDropdown = DropdownButtonFormField(
|
var intervalDropdown = DropdownButtonFormField(
|
||||||
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')),
|
||||||
value: settingsProvider.updateInterval,
|
value: settingsProvider.updateInterval,
|
||||||
@ -138,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) {
|
||||||
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,16 @@ 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()
|
||||||
],
|
],
|
||||||
))),
|
))),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
@ -351,3 +401,59 @@ class _LogsDialogState extends State<LogsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CategoryEditorSelector extends StatefulWidget {
|
||||||
|
final void Function(List<String> categories)? onSelected;
|
||||||
|
final bool singleSelect;
|
||||||
|
final Set<String> preselected;
|
||||||
|
final WrapAlignment alignment;
|
||||||
|
const CategoryEditorSelector(
|
||||||
|
{super.key,
|
||||||
|
this.onSelected,
|
||||||
|
this.singleSelect = false,
|
||||||
|
this.preselected = const {},
|
||||||
|
this.alignment = WrapAlignment.start});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CategoryEditorSelector> createState() => _CategoryEditorSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
|
||||||
|
Map<String, MapEntry<int, bool>> storedValues = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
storedValues = settingsProvider.categories.map((key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
MapEntry(value,
|
||||||
|
storedValues[key]?.value ?? widget.preselected.contains(key))));
|
||||||
|
return GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormTagInput('categories',
|
||||||
|
label: tr('category'),
|
||||||
|
emptyMessage: tr('noCategories'),
|
||||||
|
defaultValue: storedValues,
|
||||||
|
alignment: widget.alignment,
|
||||||
|
deleteConfirmationMessage: MapEntry(
|
||||||
|
tr('deleteCategoriesQuestion'),
|
||||||
|
tr('categoryDeleteWarning')),
|
||||||
|
singleSelect: widget.singleSelect)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: ((values, valid, isBuilding) {
|
||||||
|
if (!isBuilding) {
|
||||||
|
storedValues =
|
||||||
|
values['categories'] as Map<String, MapEntry<int, bool>>;
|
||||||
|
settingsProvider.categories =
|
||||||
|
storedValues.map((key, value) => MapEntry(key, value.key));
|
||||||
|
if (widget.onSelected != null) {
|
||||||
|
widget.onSelected!(storedValues.keys
|
||||||
|
.where((k) => storedValues[k]!.value)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -274,9 +274,14 @@ class AppsProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getHost(String url) {
|
||||||
|
var temp = Uri.parse(url).host.split('.');
|
||||||
|
return temp.sublist(temp.length - 2).join('.');
|
||||||
|
}
|
||||||
|
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
Uri.parse(apkUrl).origin != Uri.parse(app.url).origin &&
|
getHost(apkUrl) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -308,7 +313,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) {
|
||||||
@ -321,7 +327,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
appsToInstall.add(id);
|
appsToInstall.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (apps[id]!.app.trackOnly) {
|
if (trackOnly) {
|
||||||
trackOnlyAppsToUpdate.add(id);
|
trackOnlyAppsToUpdate.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -446,9 +452,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 &&
|
||||||
@ -456,7 +463,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) {
|
||||||
@ -465,7 +473,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) ??
|
||||||
@ -618,12 +627,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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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,41 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown(
|
||||||
|
'category',
|
||||||
|
label: tr('category'),
|
||||||
|
[
|
||||||
|
MapEntry('', tr('noCategory')),
|
||||||
|
...categories.entries.map((e) => MapEntry(e.key, e.key)).toList()
|
||||||
|
],
|
||||||
|
defaultValue: initCategory);
|
||||||
|
|
||||||
|
String? get forcedLocale {
|
||||||
|
var fl = prefs?.getString('forcedLocale');
|
||||||
|
return supportedLocales
|
||||||
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,18 @@ import 'package:html/dom.dart';
|
|||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/fdroidRepo.dart';
|
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
|
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;
|
||||||
@ -43,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;
|
String? category;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -56,17 +58,52 @@ 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.category});
|
||||||
|
|
||||||
@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) {
|
||||||
|
var source = SourceProvider().getSource(json['url']);
|
||||||
|
var formItems = source.combinedAppSpecificSettingFormItems
|
||||||
|
.reduce((value, element) => [...value, ...element]);
|
||||||
|
Map<String, dynamic> additionalSettings =
|
||||||
|
getDefaultValuesFromFormItems([formItems]);
|
||||||
|
if (json['additionalSettings'] != null) {
|
||||||
|
additionalSettings.addEntries(
|
||||||
|
Map<String, dynamic>.from(jsonDecode(json['additionalSettings']))
|
||||||
|
.entries);
|
||||||
|
}
|
||||||
|
// If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
|
||||||
|
if (json['additionalData'] != null) {
|
||||||
|
List<String> temp = List<String>.from(jsonDecode(json['additionalData']));
|
||||||
|
temp.asMap().forEach((i, value) {
|
||||||
|
if (i < formItems.length) {
|
||||||
|
if (formItems[i] is GeneratedFormSwitch) {
|
||||||
|
additionalSettings[formItems[i].key] = value == 'true';
|
||||||
|
} else {
|
||||||
|
additionalSettings[formItems[i].key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
additionalSettings['trackOnly'] =
|
||||||
|
json['trackOnly'] == '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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return App(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
json['url'] as String,
|
json['url'] as String,
|
||||||
json['author'] as String,
|
json['author'] as String,
|
||||||
@ -78,17 +115,16 @@ class App {
|
|||||||
json['apkUrls'] == null
|
json['apkUrls'] == null
|
||||||
? []
|
? []
|
||||||
: List<String>.from(jsonDecode(json['apkUrls'])),
|
: List<String>.from(jsonDecode(json['apkUrls'])),
|
||||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
json['preferredApkIndex'] == null
|
||||||
json['additionalData'] == null
|
? 0
|
||||||
? SourceProvider()
|
: json['preferredApkIndex'] as int,
|
||||||
.getSource(json['url'])
|
additionalSettings,
|
||||||
.additionalSourceAppSpecificDefaults
|
|
||||||
: List<String>.from(jsonDecode(json['additionalData'])),
|
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||||
json['pinned'] ?? false,
|
json['pinned'] ?? false,
|
||||||
json['trackOnly'] ?? false);
|
category: json['category']);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@ -99,20 +135,20 @@ 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
|
'category': category
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the input is starts with HTTPS and has no WWW
|
// Ensure the input is starts with HTTPS and has no WWW
|
||||||
preStandardizeUrl(String url) {
|
preStandardizeUrl(String url) {
|
||||||
url = url.toLowerCase();
|
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||||
if (url.indexOf('http://') != 0 && url.indexOf('https://') != 0) {
|
url.toLowerCase().indexOf('https://') != 0) {
|
||||||
url = 'https://$url';
|
url = 'https://$url';
|
||||||
}
|
}
|
||||||
if (url.indexOf('https://www.') == 0) {
|
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||||
url = 'https://${url.substring(12)}';
|
url = 'https://${url.substring(12)}';
|
||||||
}
|
}
|
||||||
url = url
|
url = url
|
||||||
@ -123,7 +159,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) =>
|
||||||
@ -136,6 +172,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;
|
||||||
@ -150,23 +193,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 = [];
|
||||||
@ -185,7 +238,7 @@ class AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? tryInferringAppId(String standardUrl,
|
String? tryInferringAppId(String standardUrl,
|
||||||
{List<String> additionalData = const []}) {
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +265,8 @@ class SourceProvider {
|
|||||||
Signal(),
|
Signal(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
FDroidRepo()
|
FDroidRepo(),
|
||||||
|
SteamMobile()
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add more mass url source classes here so they are available via the service
|
// Add more mass url source classes here so they are available via the service
|
||||||
@ -244,10 +298,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) {
|
if (element is GeneratedFormTextField && element.required) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,37 +326,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,
|
||||||
additionalData,
|
additionalSettings,
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
pinned,
|
currentApp?.pinned ?? false,
|
||||||
trackOnly);
|
category: currentApp?.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
@ -314,7 +376,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});
|
||||||
}
|
}
|
||||||
|
26
pubspec.lock
26
pubspec.lock
@ -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:
|
||||||
@ -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,7 +552,7 @@ 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:
|
||||||
@ -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:
|
||||||
@ -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.0"
|
||||||
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.0.0"
|
||||||
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.0"
|
||||||
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:
|
||||||
|
@ -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.13+77 # When changing this, update the tag in main() accordingly
|
version: 0.9.8+96 # 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
|
||||||
|
Reference in New Issue
Block a user