Compare commits

...

41 Commits

Author SHA1 Message Date
Imran Remtulla
2ff6acb701 Merge pull request #182 from ImranR98/dev
Broke `GeneratedFormItem` into sub-types + bugfix
2022-12-21 18:26:15 -05:00
Imran Remtulla
0c2d6ce84d Increment version 2022-12-21 18:23:55 -05:00
Imran Remtulla
9072862862 Broke GeneratedFormItem into sub-types
Prep for "chips" input type
2022-12-21 18:23:25 -05:00
Imran Remtulla
3cbaac2f5d Increment version 2022-12-21 15:08:18 -05:00
Imran Remtulla
0f8871efcb Merge pull request #179 from bluefly000/japanese-translation
Update Japanese translation
2022-12-21 15:07:33 -05:00
Imran Remtulla
ee216cbbba Merge pull request #181 from ImranR98/dev
Bugfix (#178) + translation typos
2022-12-21 15:07:26 -05:00
Imran Remtulla
ebe5b79dc5 Bugfix (#178) + translation typos 2022-12-21 15:06:54 -05:00
bluefly000
60014c864c Update Japanese translation 2022-12-21 21:48:32 +09:00
Imran Remtulla
070b6033bd Merge pull request #177 from ImranR98/dev
Added very basic categorization support
2022-12-21 04:25:45 -05:00
Imran Remtulla
626bebbe5a Localized new strings 2022-12-21 04:24:17 -05:00
Imran Remtulla
118460ccb9 Added category filter 2022-12-21 04:15:39 -05:00
Imran Remtulla
26f953dbb0 Category displayed on App/Apps pages
+ category save bugfix
2022-12-21 03:57:08 -05:00
Imran Remtulla
99d7595f2d Added category add/remove (no recolour/rename for now) 2022-12-21 03:08:56 -05:00
Imran Remtulla
e2f99c5e71 Increment version 2022-12-20 21:12:50 -05:00
Imran Remtulla
1f582d239b Merge pull request #175 from RanTranslations/main
assests: Update Simplified Chinese
2022-12-20 21:10:48 -05:00
Imran Remtulla
5e6b00718e Merge pull request #176 from bluefly000/main
Update Japanese translation
2022-12-20 21:10:26 -05:00
bluefly000
56594e6b19 Update Japanese translation 2022-12-21 10:44:18 +09:00
JohnsonRan
bbcc3ff9b3 assests: Update Simplified Chinese 2022-12-21 09:07:31 +08:00
Imran Remtulla
ee66c53320 Updated plugins 2022-12-20 19:06:49 -05:00
Imran Remtulla
b7d581f8b0 GitHub prereleases now not included by default 2022-12-20 18:48:54 -05:00
Imran Remtulla
ead63ba21d Translation typos 2022-12-20 18:41:02 -05:00
Imran Remtulla
c69404363f Increment version 2022-12-20 18:34:47 -05:00
Imran Remtulla
99d0bd2461 Merge pull request #173 from atilluF/main
Fix Italian translation
2022-12-20 18:33:55 -05:00
Imran Remtulla
54efda3eea Merge pull request #171 from markus-gitdev/main
Improving german translation
2022-12-20 18:33:47 -05:00
Imran Remtulla
d76d68329c Merge pull request #174 from ImranR98/dev
"Additional Settings" related code changes for maintainability + other changes
2022-12-20 18:32:46 -05:00
Imran Remtulla
b151eb27e1 Translations + bugfix 2022-12-20 18:19:44 -05:00
Imran Remtulla
6a21045e5b Progress 2022-12-20 18:00:22 -05:00
atilluF
6aedd9ce37 Update it.json (small fixes) 2022-12-20 18:48:33 +01:00
Markus
f319639a99 Merge branch 'ImranR98:main' into main 2022-12-20 08:58:43 +01:00
Markus
92e6798809 Update de.json
Abbreviating some texts to provide a better appearance.
2022-12-20 08:56:46 +01:00
Imran Remtulla
9a129d41df Added migration code for additionalData (NOTHING TESTED) 2022-12-19 20:14:54 -05:00
Imran Remtulla
0c2654a226 More fixes to prev commit 2022-12-19 19:58:12 -05:00
Imran Remtulla
afc8e41171 Made defaultvallue part of formitem 2022-12-19 19:48:37 -05:00
Imran Remtulla
1fe9e4f91e Started switching additionaldata to map 2022-12-19 19:34:43 -05:00
Imran Remtulla
dbd6dec0a6 Merge remote-tracking branch 'origin/main' into dev 2022-12-19 16:19:59 -05:00
Imran Remtulla
d068db2a57 Increment version 2022-12-19 16:19:38 -05:00
Imran Remtulla
dd5c5fd2bc Merge pull request #169 from markus-gitdev/main
German translation
2022-12-19 16:12:32 -05:00
Imran Remtulla
ac9dadd9d0 Merge pull request #168 from gidano/main
HU Text correction
2022-12-19 16:11:50 -05:00
Markus
bb0540b644 German translation
Initial version of german translation
2022-12-19 10:05:21 +01:00
gidano
819334021a HU Text correction 2022-12-19 08:06:59 +01:00
Imran Remtulla
63034dd3f9 Added 'no version detection' option 2022-12-18 02:46:25 -05:00
30 changed files with 1252 additions and 616 deletions

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

@@ -0,0 +1,254 @@
{
"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": "Install",
"markInstalled": "Mark Installed",
"update": "Update",
"markUpdated": "Mark Updated",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Disable Version Detection",
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
"downloadingX": "Downloading {}",
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
"noAPKFound": "No APK found",
"noVersionDetection": "No version detection",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"deleteCategoryQuestion": "Delete Category?",
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
"addCategory": "Add Category",
"label": "Label",
"tooManyRequestsTryAgainInMinutes": {
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minute geplant",
"other": "Bei der Aktualisierungsprüfung im Hintergrund wurde ein {} festgestellt, eine erneute Prüfung wird in {} Minuten geplant"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "Hintergrundaktualisierungsprüfung fand {} Aktualisierung - benachrichtigt den Benutzer, falls erforderlich",
"other": "Hintergrundaktualisierungsprüfung fand {} Aktualisierungen - benachrichtigt den Benutzer, falls erforderlich"
},
"apps": {
"one": "{} App",
"other": "{} Apps"
},
"url": {
"one": "{} URL",
"other": "{} URLs"
},
"minute": {
"one": "{} Minute",
"other": "{} Minutes"
},
"hour": {
"one": "{} Stunde",
"other": "{} Stunden"
},
"day": {
"one": "{} Tag",
"other": "{} Tage"
},
"clearedNLogsBeforeXAfterY": {
"one": "{n} Protokoll gelöscht (vorher = {vorher}, nachher = {nachher})",
"other": "{n} Protokolle gelöscht (vorher = {vorher}, nachher = {nachher})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} und 1 weitere App haben Aktualisierungen.",
"other": "{} und {} weitere Apps haben Aktualisierungen."
},
"xAndNMoreUpdatesInstalled": {
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
}
}

View File

@@ -37,7 +37,7 @@
"xIsTrackOnly": "{} is Track-Only", "xIsTrackOnly": "{} is Track-Only",
"source": "Source", "source": "Source",
"app": "App", "app": "App",
"appsFromSourceAreTrackOnly": "Apps from this source are 'Track-Only'.' ", "appsFromSourceAreTrackOnly": "Apps from this source are 'Track-Only'.",
"youPickedTrackOnly": "You have selected the 'Track-Only' option.", "youPickedTrackOnly": "You have selected the 'Track-Only' option.",
"trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.", "trackOnlyAppDescription": "The App will be tracked for updates, but Obtainium will not be able to download or install it.",
"cancelled": "Cancelled", "cancelled": "Cancelled",
@@ -135,7 +135,7 @@
"appearance": "Appearance", "appearance": "Appearance",
"showWebInAppView": "Show Source Webpage in App View", "showWebInAppView": "Show Source Webpage in App View",
"pinUpdates": "Pin Updates to Top of Apps View", "pinUpdates": "Pin Updates to Top of Apps View",
"updates": "Updated", "updates": "Updates",
"sourceSpecific": "Source-Specific", "sourceSpecific": "Source-Specific",
"appSource": "App Source", "appSource": "App Source",
"noLogs": "No Logs", "noLogs": "No Logs",
@@ -188,6 +188,25 @@
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Install",
"markInstalled": "Mark Installed",
"update": "Update",
"markUpdated": "Mark Updated",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Disable Version Detection",
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
"downloadingX": "Downloading {}",
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
"noAPKFound": "No APK found",
"noVersionDetection": "No version detection",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"deleteCategoryQuestion": "Delete Category?",
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
"addCategory": "Add Category",
"label": "Label",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Too many requests (rate limited) - try again in {} minute", "one": "Too many requests (rate limited) - try again in {} minute",
"other": "Too many requests (rate limited) - try again in {} minutes" "other": "Too many requests (rate limited) - try again in {} minutes"

View File

@@ -37,7 +37,7 @@
"xIsTrackOnly": "{} csak nyomon követhető", "xIsTrackOnly": "{} csak nyomon követhető",
"source": "Forrás", "source": "Forrás",
"app": "App", "app": "App",
"appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.' ", "appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.",
"youPickedTrackOnly": "A 'Csak követés' opciót választotta.", "youPickedTrackOnly": "A 'Csak követés' opciót választotta.",
"trackOnlyAppDescription": "Az alkalmazás frissítéseit nyomon követi, de az Obtainium nem tudja letölteni vagy telepíteni.", "trackOnlyAppDescription": "Az alkalmazás frissítéseit nyomon követi, de az Obtainium nem tudja letölteni vagy telepíteni.",
"cancelled": "Törölve", "cancelled": "Törölve",
@@ -133,9 +133,9 @@
"bgUpdateCheckInterval": "Háttérfrissítés ellenőrzési időköz", "bgUpdateCheckInterval": "Háttérfrissítés ellenőrzési időköz",
"neverManualOnly": "Soha csak manuális", "neverManualOnly": "Soha csak manuális",
"appearance": "Megjelenés", "appearance": "Megjelenés",
"showWebInAppView": "Forrás webhely megjelenítése az Appok nézetben", "showWebInAppView": "Forrás megjelenítése az Appok nézetben",
"pinUpdates": "Frissítések kitűzése az App nézet tetejére", "pinUpdates": "Frissítések kitűzése az App nézet tetejére",
"updates": "Frissítve", "updates": "Frissítések",
"sourceSpecific": "Forrás-specifikus", "sourceSpecific": "Forrás-specifikus",
"appSource": "App forrás", "appSource": "App forrás",
"noLogs": "Nincsenek naplók", "noLogs": "Nincsenek naplók",
@@ -188,6 +188,25 @@
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Install",
"markInstalled": "Mark Installed",
"update": "Update",
"markUpdated": "Mark Updated",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Disable Version Detection",
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
"downloadingX": "Downloading {}",
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
"noAPKFound": "No APK found",
"noVersionDetection": "No version detection",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"deleteCategoryQuestion": "Delete Category?",
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
"addCategory": "Add Category",
"label": "Label",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva", "one": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva",
"other": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva" "other": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva"

View File

@@ -22,13 +22,13 @@
"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": "i repository stellati da GitHub",
@@ -47,7 +47,7 @@
"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:",
@@ -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",
@@ -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 in alto gli aggiornamenti disponibili",
"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",
@@ -184,10 +184,29 @@
"appIdOrName": "ID o nome dell'App", "appIdOrName": "ID o nome dell'App",
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome", "appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
"reposHaveMultipleApps": "I repository possono contenere più App", "reposHaveMultipleApps": "I repository possono contenere più App",
"fdroidThirdPartyRepo": "Repository di terze parti di F-Droid", "fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Install",
"markInstalled": "Mark Installed",
"update": "Update",
"markUpdated": "Mark Updated",
"additionalOptions": "Additional Options",
"disableVersionDetection": "Disable Version Detection",
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
"downloadingX": "Downloading {}",
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
"noAPKFound": "No APK found",
"noVersionDetection": "No version detection",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"deleteCategoryQuestion": "Delete Category?",
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
"addCategory": "Add Category",
"label": "Label",
"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"

View File

@@ -16,7 +16,7 @@
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
"firstRun": "これがObtainiumの最初の実行です", "firstRun": "これがObtainiumの最初の実行です",
"settingUpdateCheckIntervalTo": "更新間隔を{}に設定する", "settingUpdateCheckIntervalTo": "確認間隔を{}に設定する",
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
"githubPATFormat": "ユーザー名:トークン", "githubPATFormat": "ユーザー名:トークン",
@@ -29,16 +29,16 @@
"cancel": "キャンセル", "cancel": "キャンセル",
"continue": "続ける", "continue": "続ける",
"requiredInBrackets": "(必須)", "requiredInBrackets": "(必須)",
"dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのoptが必要です", "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です",
"colour": "カラー", "colour": "カラー",
"githubStarredRepos": "Githubでスターしたリポジトリ", "githubStarredRepos": "Githubでスターしたリポジトリ",
"uname": "ユーザー名", "uname": "ユーザー名",
"wrongArgNum": "提供する引数の数が間違っています", "wrongArgNum": "提供する引数の数が間違っています",
"xIsTrackOnly": "{} は'追跡のみ'です", "xIsTrackOnly": "{} は追跡のみです",
"source": "ソース", "source": "ソース",
"app": "アプリ", "app": "アプリ",
"appsFromSourceAreTrackOnly": "このソースからのアプリは'追跡のみ'です'。", "appsFromSourceAreTrackOnly": "このソースからのアプリは追跡のみです。",
"youPickedTrackOnly": "'追跡のみ'を選択しています", "youPickedTrackOnly": "追跡のみを選択しています",
"trackOnlyAppDescription": "アプリのアップデートは追跡されますが、Obtainiumはアプリのダウンロードやインストールをすることはできません。", "trackOnlyAppDescription": "アプリのアップデートは追跡されますが、Obtainiumはアプリのダウンロードやインストールをすることはできません。",
"cancelled": "キャンセルしました", "cancelled": "キャンセルしました",
"appAlreadyAdded": "アプリはすでに追加されています", "appAlreadyAdded": "アプリはすでに追加されています",
@@ -124,13 +124,13 @@
"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": "アプリビューにソースウェブページを表示する",
@@ -152,11 +152,11 @@
"sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?", "sourceIsXButPackageFromYPrompt": "アプリのソースは'{}'ですが、リリースパッケージは'{}'から来ています。続行しますか?",
"updatesAvailable": "アップデートが利用可能", "updatesAvailable": "アップデートが利用可能",
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
"noNewUpdates": "新しいアップデートはありません", "noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{}のアップデートが利用可能です", "xHasAnUpdate": "{}のアップデートが利用可能です",
"appsUpdated": "アプリをアップデートしました", "appsUpdated": "アプリをアップデートしました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{}が{}にアップデートされました", "xWasUpdatedToY": "{}が{}にアップデートされました",
"errorCheckingUpdates": "アップデート確認中のエラー", "errorCheckingUpdates": "アップデート確認中のエラー",
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知", "errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
"appsRemoved": "削除されたアプリ", "appsRemoved": "削除されたアプリ",
@@ -173,7 +173,7 @@
"versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)", "versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)",
"unknown": "不明", "unknown": "不明",
"none": "なし", "none": "なし",
"never": "Never", "never": "しない",
"latestVersionX": "最新のバージョン: {}", "latestVersionX": "最新のバージョン: {}",
"installedVersionX": "インストールされたバージョン: {}", "installedVersionX": "インストールされたバージョン: {}",
"lastUpdateCheckX": "最終アップデート確認: {}", "lastUpdateCheckX": "最終アップデート確認: {}",
@@ -181,24 +181,43 @@
"removeAppQuestion": "アプリを削除しますか?", "removeAppQuestion": "アプリを削除しますか?",
"yesMarkUpdated": "はい、アップデート済みとしてマークします", "yesMarkUpdated": "はい、アップデート済みとしてマークします",
"fdroid": "F-Droid", "fdroid": "F-Droid",
"appIdOrName": "アプリIDまたは名前", "appIdOrName": "アプリIDまたは名前",
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした", "appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります", "reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo", "fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "インストール",
"markInstalled": "インストール済みとしてマークする",
"update": "アップデート",
"markUpdated": "アップデート済みとしてマークする",
"additionalOptions": "追加のオプション",
"disableVersionDetection": "バージョン検出を無効にする",
"noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。",
"downloadingX": "{} をダウンロード中",
"downloadNotifDescription": "アプリのダウンロード状況を通知する",
"noAPKFound": "APKが見つかりません",
"noVersionDetection": "バージョン検出を行わない",
"categorize": "カテゴライズ",
"categories": "カテゴリ",
"category": "カテゴリ",
"noCategory": "カテゴリなし",
"deleteCategoryQuestion": "カテゴリを削除しますか?",
"categoryDeleteWarning": "「{}」内のすべてのアプリは未分類に設定されます。",
"addCategory": "カテゴリを追加",
"label": "ラベル",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
}, },
"bgUpdateGotErrorRetryInMinutes": { "bgUpdateGotErrorRetryInMinutes": {
"one": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します", "one": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します",
"other": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します" "other": "バックグラウンドのアップデート確認で {} の問題が発生, {} 分後に再試行します"
}, },
"bgCheckFoundUpdatesWillNotifyIfNeeded": { "bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します", "one": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します",
"other": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" "other": "バックグラウンドのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します"
}, },
"apps": { "apps": {
"one": "{}個のアプリ", "one": "{}個のアプリ",
@@ -225,11 +244,11 @@
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})" "other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{}とさらに{}個のアプリのアップデートが利用可能です", "one": "{}とさらに{}個のアプリのアップデートが利用可能です",
"other": "{}とさらに{}個のアプリのアップデートが利用可能です" "other": "{}とさらに{}個のアプリのアップデートが利用可能です"
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{}とさらに{}個のアプリがアップデートされました", "one": "{}とさらに{}個のアプリがアップデートされました",
"other": "{}とさらに{}個のアプリがアップデートされました" "other": "{}とさらに{}個のアプリがアップデートされました"
} }
} }

View File

@@ -22,9 +22,9 @@
"githubPATFormat": "username:token", "githubPATFormat": "username:token",
"githubPATLinkText": "关于 GitHub 个人访问令牌", "githubPATLinkText": "关于 GitHub 个人访问令牌",
"includePrereleases": "包含预发布版", "includePrereleases": "包含预发布版",
"fallbackToOlderReleases": "回到旧版", "fallbackToOlderReleases": "回退到旧版",
"filterReleaseTitlesByRegEx": "通过正则表达式过滤发布标题", "filterReleaseTitlesByRegEx": "使用正则以过滤发布标题",
"invalidRegEx": "无效的正则表达式", "invalidRegEx": "表达式无效",
"noDescription": "无描述", "noDescription": "无描述",
"cancel": "取消", "cancel": "取消",
"continue": "继续", "continue": "继续",
@@ -109,7 +109,7 @@
"searchX": "搜索 {}", "searchX": "搜索 {}",
"noResults": "无结果", "noResults": "无结果",
"importX": "导入 {}", "importX": "导入 {}",
"importedAppsIdDisclaimer": "导入的应用程序可能不正确地显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。", "importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
"importErrors": "导入错误", "importErrors": "导入错误",
"importedXOfYApps": "{} 中的 {} 个应用已导入", "importedXOfYApps": "{} 中的 {} 个应用已导入",
"followingURLsHadErrors": "以下 URL 有错误:", "followingURLsHadErrors": "以下 URL 有错误:",
@@ -134,7 +134,7 @@
"neverManualOnly": "手动", "neverManualOnly": "手动",
"appearance": "外观", "appearance": "外观",
"showWebInAppView": "在应用来源页显示网页", "showWebInAppView": "在应用来源页显示网页",
"pinUpdates": "将需要更新的应用固定到顶部", "pinUpdates": "更新的应用置顶",
"updates": "检查间隔", "updates": "检查间隔",
"sourceSpecific": "Github 访问令牌", "sourceSpecific": "Github 访问令牌",
"appSource": "源代码", "appSource": "源代码",
@@ -145,9 +145,9 @@
"appNotFound": "未找到应用", "appNotFound": "未找到应用",
"obtainiumExportHyphenatedLowercase": "obtainium-导出", "obtainiumExportHyphenatedLowercase": "obtainium-导出",
"pickAnAPK": "选择一个安装包", "pickAnAPK": "选择一个安装包",
"appHasMoreThanOnePackage": "{} 有多于一个安装包", "appHasMoreThanOnePackage": "{} 有多个架构可用",
"deviceSupportsXArch": "你的设备支持 {} CPU 架构", "deviceSupportsXArch": "你的设备支持 {} 架构",
"deviceSupportsFollowingArchs": "你的设备支持以下 CPU 架构:", "deviceSupportsFollowingArchs": "你的设备支持以下架构:",
"warning": "警告", "warning": "警告",
"sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?", "sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?",
"updatesAvailable": "更新可用", "updatesAvailable": "更新可用",
@@ -188,6 +188,25 @@
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "安装",
"markInstalled": "标记为已安装",
"update": "更新",
"markUpdated": "标记为已更新",
"additionalOptions": "附加选项",
"disableVersionDetection": "关闭版本检测",
"noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
"downloadingX": "下载中 {}",
"downloadNotifDescription": "通知用户下载进度",
"noAPKFound": "未找到安装包",
"noVersionDetection": "无版本检测",
"categorize": "Categorize",
"categories": "Categories",
"category": "Category",
"noCategory": "No Category",
"deleteCategoryQuestion": "Delete Category?",
"categoryDeleteWarning": "All Apps in {} will be set to uncategorized.",
"addCategory": "Add Category",
"label": "Label",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "请求过多 (API 限制) - 在 {} 分钟后重试", "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
"other": "请求过多 (API 限制) - 在 {} 分钟后重试" "other": "请求过多 (API 限制) - 在 {} 分钟后重试"

View File

@@ -25,8 +25,9 @@ class APKMirror extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/feed')); Response res = await get(Uri.parse('$standardUrl/feed'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
String? titleString = parse(res.body) String? titleString = parse(res.body)

View File

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

View File

@@ -9,13 +9,12 @@ class FDroidRepo extends AppSource {
FDroidRepo() { FDroidRepo() {
name = tr('fdroidThirdPartyRepo'); name = tr('fdroidThirdPartyRepo');
additionalSourceAppSpecificFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
GeneratedFormItem( GeneratedFormTextField('appIdOrName',
label: tr('appIdOrName'), label: tr('appIdOrName'),
hint: tr('reposHaveMultipleApps'), hint: tr('reposHaveMultipleApps'),
required: true, required: true)
key: 'appIdOrName')
] ]
]; ];
} }
@@ -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();
} }

View File

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

View File

@@ -25,8 +25,9 @@ class GitLab extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl); var standardUri = Uri.parse(standardUrl);

View File

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

View File

@@ -24,8 +24,9 @@ class Mullvad extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/en/download/android')); Response res = await get(Uri.parse('$standardUrl/en/download/android'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var version = parse(res.body) var version = parse(res.body)

View File

@@ -18,8 +18,9 @@ class Signal extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
Response res = Response res =
await get(Uri.parse('https://updates.$host/android/latest.json')); await get(Uri.parse('https://updates.$host/android/latest.json'));
if (res.statusCode == 200) { if (res.statusCode == 200) {

View File

@@ -23,8 +23,9 @@ class SourceForge extends AppSource {
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, List<String> additionalData, String standardUrl,
{bool trackOnly = false}) async { Map<String, dynamic> additionalSettings,
) async {
Response res = await get(Uri.parse('$standardUrl/rss?path=/')); Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);

View File

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

View File

@@ -1,50 +1,96 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum FormItemType { string, bool } abstract class GeneratedFormItem {
typedef OnValueChanges = void Function(
List<String> values, bool valid, bool isBuilding);
class GeneratedFormItem {
late String key; late String key;
late String label; late String label;
late FormItemType type; late List<Widget> belowWidgets;
late dynamic defaultValue;
List<dynamic> additionalValidators;
dynamic ensureType(dynamic val);
GeneratedFormItem(this.key,
{this.label = 'Input',
this.belowWidgets = const [],
this.defaultValue,
this.additionalValidators = const []});
}
class GeneratedFormTextField extends GeneratedFormItem {
late bool required; late bool required;
late int max; late int max;
late List<String? Function(String? value)> additionalValidators;
late String id;
late List<Widget> belowWidgets;
late String? hint; late String? hint;
late List<MapEntry<String, String>>? opts;
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';
}
}
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 +98,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>)
@@ -80,42 +125,41 @@ class _GeneratedFormState extends State<GeneratedForm> {
super.initState(); super.initState();
// Initialize form values as all empty // Initialize form values as all empty
values.clear();
int j = 0; int j = 0;
values = widget.items for (var row in widget.items) {
.map((row) => row.map((e) { for (var e in row) {
return j < widget.defaultValues.length values[e.key] = e.defaultValue;
? widget.defaultValues[j++] }
: e.opts != null }
? e.opts!.first.key
: '';
}).toList())
.toList();
// Dynamically create form inputs // Dynamically create form inputs
formInputs = widget.items.asMap().entries.map((row) { formInputs = widget.items.asMap().entries.map((row) {
return row.value.asMap().entries.map((e) { return row.value.asMap().entries.map((e) {
if (e.value.type == FormItemType.string && e.value.opts == null) { var formItem = e.value;
if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>(); final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField( return TextFormField(
key: formFieldKey, key: formFieldKey,
initialValue: values[row.key][e.key], initialValue: values[formItem.key],
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
values[row.key][e.key] = value; values[formItem.key] = value;
someValueChanged(); someValueChanged();
}); });
}, },
decoration: InputDecoration( decoration: InputDecoration(
helperText: e.value.label + (e.value.required ? ' *' : ''), helperText: formItem.label + (formItem.required ? ' *' : ''),
hintText: e.value.hint), hintText: formItem.hint),
minLines: e.value.max <= 1 ? null : e.value.max, minLines: formItem.max <= 1 ? null : formItem.max,
maxLines: e.value.max <= 1 ? 1 : e.value.max, maxLines: formItem.max <= 1 ? 1 : formItem.max,
validator: (value) { validator: (value) {
if (e.value.required && (value == null || value.trim().isEmpty)) { if (formItem.required &&
return '${e.value.label} ${tr('requiredInBrackets')}'; (value == null || value.trim().isEmpty)) {
return '${formItem.label} ${tr('requiredInBrackets')}';
} }
for (var validator in e.value.additionalValidators) { for (var validator in formItem.additionalValidators) {
String? result = validator(value); String? result = validator(value);
if (result != null) { if (result != null) {
return result; return result;
@@ -124,21 +168,20 @@ class _GeneratedFormState extends State<GeneratedForm> {
return null; return null;
}, },
); );
} else if (e.value.type == FormItemType.string && } else if (formItem is GeneratedFormDropdown) {
e.value.opts != null) { if (formItem.opts!.isEmpty) {
if (e.value.opts!.isEmpty) {
return Text(tr('dropdownNoOptsError')); return Text(tr('dropdownNoOptsError'));
} }
return DropdownButtonFormField( return DropdownButtonFormField(
decoration: InputDecoration(labelText: e.value.label), decoration: InputDecoration(labelText: formItem.label),
value: values[row.key][e.key], value: values[formItem.key],
items: e.value.opts! items: formItem.opts!
.map((e) => .map((e2) =>
DropdownMenuItem(value: e.key, child: Text(e.value))) DropdownMenuItem(value: e2.key, child: Text(e2.value)))
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
values[row.key][e.key] = value ?? e.value.opts!.first.key; values[formItem.key] = value ?? formItem.opts!.first.key;
someValueChanged(); someValueChanged();
}); });
}); });
@@ -154,16 +197,16 @@ 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();
}); });
}) })
@@ -178,9 +221,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
if (rowInputs.key > 0) { if (rowInputs.key > 0) {
rows.add([ rows.add([
SizedBox( SizedBox(
height: widget.items[rowInputs.key][0].type == FormItemType.bool && height: widget.items[rowInputs.key][0] is GeneratedFormSwitch &&
widget.items[rowInputs.key - 1][0].type == widget.items[rowInputs.key - 1][0] is! GeneratedFormSwitch
FormItemType.string
? 25 ? 25
: 8, : 8,
) )
@@ -217,18 +259,3 @@ class _GeneratedFormState extends State<GeneratedForm> {
)); ));
} }
} }
String? findGeneratedFormValueByKey(
List<GeneratedFormItem> items, List<String> values, String key) {
var foundIndex = -1;
for (var i = 0; i < items.length; i++) {
if (items[i].key == key) {
foundIndex = i;
break;
}
}
if (foundIndex >= 0 && foundIndex < values.length) {
return values[foundIndex];
}
return null;
}

View File

@@ -8,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(

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.8.20'; const String currentVersion = '0.9.2';
const String currentReleaseTag = const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
@@ -32,7 +32,8 @@ const supportedLocales = [
Locale('zh'), Locale('zh'),
Locale('it'), Locale('it'),
Locale('ja'), Locale('ja'),
Locale('hu') Locale('hu'),
Locale('de')
]; ];
const fallbackLocale = Locale('en'); const fallbackLocale = Locale('en');
const localeDir = 'assets/translations'; const localeDir = 'assets/translations';
@@ -199,9 +200,8 @@ class _ObtainiumState extends State<Obtainium> {
currentReleaseTag, currentReleaseTag,
[], [],
0, 0,
['true'], {'includePrereleases': true},
null, null,
false,
false) false)
]); ]);
} }

View File

@@ -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,46 +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) {
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) {
setState(() {
otherAdditionalData = values;
otherAdditionalDataIsValid = valid;
});
}
},
defaultValues: pickedSource!
.additionalAppSpecificSourceAgnosticDefaults),
], ],
) )
else else

View File

@@ -1,6 +1,7 @@
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';
@@ -33,6 +34,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 +42,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 +91,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 +132,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 +150,51 @@ 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: 32,
),
app?.app.category != null
? Chip(
label: Text(app!.app.category!),
backgroundColor:
Color(categories[app.app.category!] ?? 0x0),
onDeleted: () {
app.app.category = null;
appsProvider.saveApps([app.app]);
},
visualDensity: VisualDensity.compact,
) )
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: 'Pick a Category',
items: [
[
settingsProvider
.getCategoryFormItem()
]
]);
}).then((value) {
if (value != null && app != null) {
String? cat = (value['category']
?.isNotEmpty ??
false)
? value['category']
: null;
app.app.category = cat;
appsProvider.saveApps([app.app]);
}
});
},
child: Text(tr('categorize')))
])
], ],
)), )),
], ],
@@ -151,7 +216,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 +264,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 +313,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 +325,9 @@ class _AppPageState extends State<AppPage> {
? () { ? () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
() async { () async {
if (app?.app.trackOnly != true) { if (app?.app.additionalSettings[
'trackOnly'] !=
true) {
await settingsProvider await settingsProvider
.getInstallPermission(); .getInstallPermission();
} }
@@ -264,12 +349,12 @@ class _AppPageState extends State<AppPage> {
} }
: null, : null,
child: Text(app?.app.installedVersion == null child: Text(app?.app.installedVersion == null
? app?.app.trackOnly == false ? !trackOnly
? 'Install' ? tr('install')
: 'Mark Installed' : tr('markInstalled')
: app?.app.trackOnly == false : !trackOnly
? 'Update' ? tr('update')
: 'Mark Updated'))), : tr('markUpdated')))),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
ElevatedButton( ElevatedButton(
onPressed: app?.downloadProgress != null onPressed: app?.downloadProgress != null

View File

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

View File

@@ -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 =
@@ -346,10 +352,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) {

View File

@@ -1,8 +1,12 @@
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/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,11 +21,27 @@ 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) {
SettingsProvider settingsProvider = context.watch<SettingsProvider>(); SettingsProvider settingsProvider = context.watch<SettingsProvider>();
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
AppsProvider appsProvider = context.read<AppsProvider>();
if (settingsProvider.prefs == null) { if (settingsProvider.prefs == null) {
settingsProvider.initializeSettings(); settingsProvider.initializeSettings();
} }
@@ -138,21 +158,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();
} }
@@ -162,6 +178,8 @@ class _SettingsPageState extends State<SettingsPage> {
height: 16, height: 16,
); );
var categories = settingsProvider.categories;
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(slivers: <Widget>[ body: CustomScrollView(slivers: <Widget>[
@@ -237,6 +255,94 @@ 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,
Wrap(
children: [
...categories.entries.toList().map((e) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4),
child: Chip(
label: Text(e.key),
backgroundColor: Color(e.value),
visualDensity: VisualDensity.compact,
onDeleted: () {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr(
'deleteCategoryQuestion'),
message: tr(
'categoryDeleteWarning',
args: [e.key]),
items: []);
}).then((value) {
if (value != null) {
setState(() {
categories.remove(e.key);
settingsProvider.categories =
categories;
});
appsProvider.saveApps(appsProvider
.apps.values
.where((element) =>
element.app.category ==
e.key)
.map((e) {
var a = e.app;
a.category = null;
return a;
}).toList());
}
});
},
));
}),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4),
child: IconButton(
onPressed: () {
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('addCategory'),
items: [
[
GeneratedFormTextField(
'label',
label: tr('label'))
]
]);
}).then((value) {
String? label = value?['label'];
if (label != null) {
setState(() {
categories[label] =
generateRandomLightColor()
.value;
settingsProvider.categories =
categories;
});
}
});
},
icon: const Icon(Icons.add),
visualDensity: VisualDensity.compact,
tooltip: tr('add'),
))
],
)
], ],
))), ))),
SliverToBoxAdapter( SliverToBoxAdapter(

View File

@@ -313,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) {
@@ -326,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);
} }
} }
@@ -451,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 &&
@@ -461,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) {
@@ -470,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) ??
@@ -623,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;
} }

View File

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

View File

@@ -1,9 +1,12 @@
// 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: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 +147,20 @@ 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));
}
getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown(
'category',
label: tr('category'),
[
MapEntry('', tr('noCategory')),
...categories.entries.map((e) => MapEntry(e.key, e.key)).toList()
],
defaultValue: initCategory);
} }

View File

@@ -19,6 +19,7 @@ import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/mass_app_sources/githubstars.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart';
import 'package:obtainium/providers/settings_provider.dart';
class AppNames { class AppNames {
late String author; late String author;
@@ -44,10 +45,10 @@ class App {
late String latestVersion; late String latestVersion;
List<String> apkUrls = []; List<String> apkUrls = [];
late int preferredApkIndex; late int preferredApkIndex;
late List<String> additionalData; late Map<String, dynamic> additionalSettings;
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
bool pinned = false; bool pinned = false;
bool trackOnly = false; String? category;
App( App(
this.id, this.id,
this.url, this.url,
@@ -57,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,
@@ -79,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,
@@ -100,10 +135,10 @@ class App {
'latestVersion': latestVersion, 'latestVersion': latestVersion,
'apkUrls': jsonEncode(apkUrls), 'apkUrls': jsonEncode(apkUrls),
'preferredApkIndex': preferredApkIndex, 'preferredApkIndex': preferredApkIndex,
'additionalData': jsonEncode(additionalData), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned,
'trackOnly': trackOnly 'category': category
}; };
} }
@@ -124,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) =>
@@ -137,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;
@@ -151,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 = [];
@@ -186,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;
} }
} }
@@ -246,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 && element.opts == null) { if (element is GeneratedFormTextField && element.required) {
return true; return true;
} }
} }
@@ -274,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) {
additionalSettings['trackOnly'] = true;
}
if (noVersionDetectionOverride) {
additionalSettings['noVersionDetection'] = true;
}
var trackOnly = currentApp?.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
@@ -316,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});
} }

View File

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

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.8.20+84 # When changing this, update the tag in main() accordingly version: 0.9.2+90 # 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