mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
230 Commits
v0.11.17-b
...
v0.13.14-b
Author | SHA1 | Date | |
---|---|---|---|
3f4c6a1b76 | |||
60ad3199ca | |||
1984ffb1c0 | |||
7877a14f07 | |||
568a94968b | |||
a6a68af24e | |||
5cdd110544 | |||
5bbe306f8f | |||
48acbc563a | |||
ab1f7e7179 | |||
667e909a70 | |||
bcc0d280ab | |||
da027b7734 | |||
09056665c2 | |||
f4c3951f6d | |||
00f42bb881 | |||
d8408a26c2 | |||
ede54531c8 | |||
0fa0a4b19a | |||
af5ea3db0f | |||
e75ca05aa4 | |||
3483190b78 | |||
69656e65c3 | |||
e6c6841fac | |||
16d63a4416 | |||
2eaf443359 | |||
5979957d60 | |||
049eb5914c | |||
7577f3ac9b | |||
5b05745b02 | |||
4366b4e369 | |||
9c60f10005 | |||
a0d02043c4 | |||
ff5152bf79 | |||
995a826917 | |||
2965e159cb | |||
0dcd5163d4 | |||
d31bbd9ea8 | |||
423ba07fad | |||
3697d74185 | |||
038f089aac | |||
ba3f512445 | |||
0fc1cff0a8 | |||
40bec4b732 | |||
8ca1e09c86 | |||
e0c4ec5028 | |||
7fcba6c911 | |||
0186c00d97 | |||
9294540b5d | |||
0b16c28224 | |||
83028d405a | |||
c4262c3eaa | |||
f0e1831d30 | |||
9efd0dd46e | |||
eb26c0be0b | |||
1ff1c6ca33 | |||
6169915e63 | |||
a0d466a074 | |||
6f9ef6d51e | |||
feb4c2eabc | |||
c2cf39125d | |||
833ece1ef5 | |||
fee23cadfa | |||
4c6303f783 | |||
ce6e6c47db | |||
2ccff15525 | |||
d24f2b4e6d | |||
03fc6a530f | |||
4136734a60 | |||
ca1371260c | |||
03c2ce9a01 | |||
eda5fec37c | |||
e21c6297ff | |||
c6297ea449 | |||
e33cc00266 | |||
96c92c8df9 | |||
e256ada2dc | |||
eb0be196da | |||
1606ad3442 | |||
d212f13345 | |||
f80c9ec33e | |||
7681e23de9 | |||
22a60df40e | |||
431a01f2a5 | |||
0cd4385de7 | |||
0774b3ddc3 | |||
b60b1ed058 | |||
b196715d60 | |||
0673e90dff | |||
59cfa242fb | |||
65ab72ba90 | |||
408bca8951 | |||
480467492a | |||
219b04aedb | |||
a0709856ef | |||
577642850f | |||
e1db024034 | |||
cc268aeeda | |||
d5f7eced8b | |||
cc3c4cc79f | |||
89b61884f1 | |||
33d3fc2d8e | |||
b07f5dd6b6 | |||
b43e13bb56 | |||
3be5543df4 | |||
91ad9efa43 | |||
ee292146d1 | |||
12867634b6 | |||
2e4fe89b85 | |||
b4642e16ad | |||
8ca5964d31 | |||
30c89fe385 | |||
fb9e66332d | |||
84b512f282 | |||
6f9aa85a72 | |||
639fc20fcb | |||
75631e5c5a | |||
9ec345761e | |||
1f9c2c1699 | |||
cbec486ad1 | |||
85ef60d4a8 | |||
44bde571bf | |||
eaaee5e7cd | |||
e1980f4de2 | |||
be9c671a56 | |||
0404449842 | |||
d6366a145e | |||
0a751cf545 | |||
5885ea57ad | |||
f8b326529f | |||
9f5f1174ba | |||
779de58f74 | |||
76e316422c | |||
36273fe02d | |||
03b592521c | |||
a5ef47a060 | |||
289c801fec | |||
73d04b1564 | |||
9469d56144 | |||
d063bca474 | |||
7c592756fe | |||
08586870fb | |||
8b123acdcd | |||
08aa04f812 | |||
dd19fcf6da | |||
04b3c8ad7d | |||
81f66683d2 | |||
392554123b | |||
3e4d5c26ac | |||
86b7f6fef3 | |||
e1d914118f | |||
4a07cf9951 | |||
ce44e200a5 | |||
e8ebf53626 | |||
cdd6a4124c | |||
09c71e4e9f | |||
28a996441c | |||
396bf012c9 | |||
02da24aa75 | |||
3c6e66ce12 | |||
0213b542e3 | |||
b0e8a4a297 | |||
e72b33ebf2 | |||
283722319b | |||
b406bb5c6a | |||
de2b7fa7a1 | |||
be61220af4 | |||
3e732a4317 | |||
9f2db4e4e7 | |||
78141998f4 | |||
934f237e34 | |||
1b2a9a39e3 | |||
dc52fb6181 | |||
9e4ac397d8 | |||
0ec944eae9 | |||
ad250c30e4 | |||
1090f15508 | |||
666941350e | |||
eeadbce8b0 | |||
ce8aeff342 | |||
0d8362a2ed | |||
3b28143a4e | |||
537628f378 | |||
c92d76df98 | |||
b6959e1a8b | |||
1bf648da60 | |||
6a1275e9e4 | |||
df242b91ad | |||
7ea75325bb | |||
0704dfe2ee | |||
6275cbf114 | |||
36b8ef6782 | |||
d274b9a428 | |||
1c2980d1ac | |||
8f0aac057e | |||
e929920a48 | |||
8ed254c7dd | |||
46a00836df | |||
f144ffdded | |||
d597d569e2 | |||
b62475de87 | |||
334ac8d3d6 | |||
9193788356 | |||
8f75ddd43f | |||
a2edc86bfa | |||
0804e680b2 | |||
49affd1bd4 | |||
202ce4f0d5 | |||
361a3e1bc2 | |||
f33a26d4f4 | |||
7aaf56ec8c | |||
ed120016d9 | |||
e8cbac8657 | |||
b66c13d319 | |||
782d055bc3 | |||
d557746965 | |||
e6b05d50b9 | |||
dea635fa6a | |||
682026ed0a | |||
9fe8a200ef | |||
210100da2b | |||
d52660235b | |||
e386b5ab8a | |||
abf7be222d | |||
4c5b9304c0 | |||
4cfe6af044 | |||
3f0c4068dd | |||
7981ca29c5 | |||
187efa8fc5 | |||
cd27ff7f2d |
@ -15,9 +15,11 @@ Currently supported App sources:
|
|||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- [Signal](https://signal.org/)
|
- [Signal](https://signal.org/)
|
||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
|
- [SourceHut](https://git.sr.ht/)
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
|
- [APKPure](https://apkpure.com/)
|
||||||
- Third Party F-Droid Repos
|
- Third Party F-Droid Repos
|
||||||
- Any URLs ending with `/fdroid/<word>`, where `<word>` can be anything - most often `repo`
|
- Jenkins Jobs
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
- [Telegram App](https://telegram.org)
|
- [Telegram App](https://telegram.org)
|
||||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
||||||
@ -30,12 +32,8 @@ Currently supported App sources:
|
|||||||
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png"
|
||||||
alt="Get it on GitHub"
|
alt="Get it on GitHub"
|
||||||
height="80">](https://github.com/ImranR98/Obtainium/releases)
|
height="80">](https://github.com/ImranR98/Obtainium/releases)
|
||||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
|
|
||||||
alt="Get it on IzzyOnDroid"
|
|
||||||
height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.imranr.obtainium)
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs happen asynchronously and the success/failure of an install cannot be determined directly. This results in install statuses and versions sometimes being out of sync with the OS until the next launch or until the problem is manually corrected.
|
|
||||||
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
||||||
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action
|
||||||
|
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
|
||||||
|
android:exported="false"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
@ -46,9 +51,18 @@
|
|||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="dev.imranr.obtainium"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"/>
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
@ -2,4 +2,5 @@
|
|||||||
<paths>
|
<paths>
|
||||||
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
||||||
<external-path path="." name="external_storage_root" />
|
<external-path path="." name="external_storage_root" />
|
||||||
|
<external-path name="external_files" path="."/>
|
||||||
</paths>
|
</paths>
|
@ -26,6 +26,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
BIN
assets/graphics/icon.png
Normal file
BIN
assets/graphics/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
||||||
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
||||||
"githubPATFormat": "Benutzername:Token",
|
"githubPATFormat": "Benutzername:Token",
|
||||||
"githubPATLinkText": "Über GitHub PATs",
|
|
||||||
"includePrereleases": "Vorabversionen einbeziehen",
|
"includePrereleases": "Vorabversionen einbeziehen",
|
||||||
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
||||||
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
||||||
@ -71,7 +70,7 @@
|
|||||||
"updateX": "Aktualisiere {}",
|
"updateX": "Aktualisiere {}",
|
||||||
"installX": "Installiere {}",
|
"installX": "Installiere {}",
|
||||||
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
|
"markXTrackOnlyAsUpdated": "Markiere {}\n(Nur Nachverfolgen)\nals aktualisiert",
|
||||||
"changeX": "Ändern {}",
|
"changeX": "Ändere {}",
|
||||||
"installUpdateApps": "Apps installieren/aktualisieren",
|
"installUpdateApps": "Apps installieren/aktualisieren",
|
||||||
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
"installUpdateSelectedApps": "Ausgewählte Apps installieren/aktualisieren",
|
||||||
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
"markXSelectedAppsAsUpdated": "Markiere {} ausgewählte Apps als aktuell?",
|
||||||
@ -122,11 +121,12 @@
|
|||||||
"followSystem": "System folgen",
|
"followSystem": "System folgen",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Verwende Pure Black Dark Theme",
|
||||||
"appSortBy": "App sortieren nach",
|
"appSortBy": "App sortieren nach",
|
||||||
"authorName": "Autor/Name",
|
"authorName": "Autor/Name",
|
||||||
"nameAuthor": "Name/Autor",
|
"nameAuthor": "Name/Autor",
|
||||||
"asAdded": "Wie hinzugefügt",
|
"asAdded": "Wie hinzugefügt",
|
||||||
"appSortOrder": "App Sortierung nach",
|
"appSortOrder": "App sortieren nach",
|
||||||
"ascending": "Aufsteigend",
|
"ascending": "Aufsteigend",
|
||||||
"descending": "Absteigend",
|
"descending": "Absteigend",
|
||||||
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
"bgUpdateCheckInterval": "Prüfintervall für Hintergrundaktualisierung",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
"lastUpdateCheckX": "Letzte Aktualisierungsprüfung: {}",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
"yesMarkUpdated": "Ja, als aktualisiert markieren",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID oder Name",
|
"appIdOrName": "App ID oder Name",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
|
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
|
||||||
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
|
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
@ -207,6 +208,7 @@
|
|||||||
"addCategory": "Kategorie hinzufügen",
|
"addCategory": "Kategorie hinzufügen",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||||
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
"storagePermissionDenied": "Speicherberechtigung verweigert",
|
||||||
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
"selectedCategorizeWarning": "Dadurch werden alle bestehenden Kategorieeinstellungen für die ausgewählten Apps ersetzt.",
|
||||||
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
"filterAPKsByRegEx": "APKs nach regulärem Ausdruck filtern",
|
||||||
@ -217,13 +219,24 @@
|
|||||||
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
"releaseDateAsVersionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert, aber ein Veröffentlichungsdatum verfügbar ist.",
|
||||||
"changes": "Änderungen",
|
"changes": "Änderungen",
|
||||||
"releaseDate": "Veröffentlichungsdatum",
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
"importFromURLsInFile": "Importieren von URLs aus Datei ( z.B. OPML)",
|
"importFromURLsInFile": "Importieren von URLs aus Datei (z. B. OPML)",
|
||||||
"versionDetection": "Versionserkennung",
|
"versionDetection": "Versionserkennung",
|
||||||
"standardVersionDetection": "Standardversionserkennung",
|
"standardVersionDetection": "Standardversionserkennung",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "Nach Kategorie gruppieren",
|
||||||
|
"autoApkFilterByArch": "Nach Möglichkeit versuchen, APKs nach CPU-Architektur zu filtern",
|
||||||
|
"overrideSource": "Quelle überschreiben",
|
||||||
|
"dontShowAgain": "Nicht noch einmal zeigen",
|
||||||
|
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||||
|
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||||
|
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Aktiviert Suche)",
|
||||||
|
"about": "Über",
|
||||||
|
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||||
|
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||||
|
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "App entfernen?"
|
"other": "Apps entfernen?"
|
||||||
},
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
|
||||||
@ -247,7 +260,7 @@
|
|||||||
},
|
},
|
||||||
"minute": {
|
"minute": {
|
||||||
"one": "{} Minute",
|
"one": "{} Minute",
|
||||||
"other": "{} Minutes"
|
"other": "{} Minuten"
|
||||||
},
|
},
|
||||||
"hour": {
|
"hour": {
|
||||||
"one": "{} Stunde",
|
"one": "{} Stunde",
|
||||||
@ -269,4 +282,4 @@
|
|||||||
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
|
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
|
||||||
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
|
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
|
||||||
"githubPATHint": "PAT must be in this format: username:token",
|
"githubPATHint": "PAT must be in this format: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "About GitHub PATs",
|
|
||||||
"includePrereleases": "Include prereleases",
|
"includePrereleases": "Include prereleases",
|
||||||
"fallbackToOlderReleases": "Fallback to older releases",
|
"fallbackToOlderReleases": "Fallback to older releases",
|
||||||
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"followSystem": "Follow System",
|
"followSystem": "Follow System",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "App Sort By",
|
"appSortBy": "App Sort By",
|
||||||
"authorName": "Author/Name",
|
"authorName": "Author/Name",
|
||||||
"nameAuthor": "Name/Author",
|
"nameAuthor": "Name/Author",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "Last Update Check: {}",
|
"lastUpdateCheckX": "Last Update Check: {}",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"yesMarkUpdated": "Yes, Mark as Updated",
|
"yesMarkUpdated": "Yes, Mark as Updated",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID or Name",
|
"appIdOrName": "App ID or Name",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
"appWithIdOrNameNotFound": "No App was found with that ID or Name",
|
||||||
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
"reposHaveMultipleApps": "Repos may contain multiple Apps",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
|
||||||
@ -207,6 +208,7 @@
|
|||||||
"addCategory": "Add Category",
|
"addCategory": "Add Category",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"copiedToClipboard": "Copied to Clipboard",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
||||||
@ -221,6 +223,17 @@
|
|||||||
"versionDetection": "Version Detection",
|
"versionDetection": "Version Detection",
|
||||||
"standardVersionDetection": "Standard version detection",
|
"standardVersionDetection": "Standard version detection",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "Group by Category",
|
||||||
|
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
|
||||||
|
"overrideSource": "Override Source",
|
||||||
|
"dontShowAgain": "Don't show this again",
|
||||||
|
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
|
||||||
|
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
|
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
@ -269,4 +282,4 @@
|
|||||||
"one": "{} and 1 more app were updated.",
|
"one": "{} and 1 more app were updated.",
|
||||||
"other": "{} and {} more apps were updated."
|
"other": "{} and {} more apps were updated."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
285
assets/translations/es.json
Normal file
285
assets/translations/es.json
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "URL de la aplicación {} no válida",
|
||||||
|
"noReleaseFound": "No se ha podido encontrar una versión válida",
|
||||||
|
"noVersionFound": "No se ha podido determinar la versión de la publicación",
|
||||||
|
"urlMatchesNoSource": "La URL no coincide con ninguna fuente conocida",
|
||||||
|
"cantInstallOlderVersion": "No se puede instalar una versión previa de la aplicación",
|
||||||
|
"appIdMismatch": "La ID del paquete descargado no coincide con la ID de la aplicación instalada",
|
||||||
|
"functionNotImplemented": "Esta clase no ha implementado esta función",
|
||||||
|
"placeholder": "Espacio reservado",
|
||||||
|
"someErrors": "Han ocurrido algunos errores",
|
||||||
|
"unexpectedError": "Error Inesperado",
|
||||||
|
"ok": "Correcto",
|
||||||
|
"and": "y",
|
||||||
|
"startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano",
|
||||||
|
"bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es {}",
|
||||||
|
"startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano",
|
||||||
|
"bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano",
|
||||||
|
"firstRun": "Esta es la primera ejecución de Obtainium",
|
||||||
|
"settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}",
|
||||||
|
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||||
|
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
|
||||||
|
"githubPATFormat": "nombre_de_usuario:token",
|
||||||
|
"includePrereleases": "Incluir versiones preliminares",
|
||||||
|
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||||
|
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
||||||
|
"invalidRegEx": "Expresión regular inválida",
|
||||||
|
"noDescription": "Sin descripción",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"continue": "Continuar",
|
||||||
|
"requiredInBrackets": "(Requerido)",
|
||||||
|
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
|
||||||
|
"colour": "Color",
|
||||||
|
"githubStarredRepos": "Repositorios favoritos de GitHub",
|
||||||
|
"uname": "Nombre de usuario",
|
||||||
|
"wrongArgNum": "Número de argumentos provistos inválido",
|
||||||
|
"xIsTrackOnly": "{} es de 'Solo Seguimiento'",
|
||||||
|
"source": "Origen",
|
||||||
|
"app": "Aplicación",
|
||||||
|
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
|
||||||
|
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
|
||||||
|
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
|
||||||
|
"cancelled": "Cancelado",
|
||||||
|
"appAlreadyAdded": "Aplicación ya añadida",
|
||||||
|
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
|
||||||
|
"addApp": "Añadir Aplicación",
|
||||||
|
"appSourceURL": "URL de Origen de la Aplicación",
|
||||||
|
"error": "Error",
|
||||||
|
"add": "Añadir",
|
||||||
|
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
||||||
|
"search": "Buscar",
|
||||||
|
"additionalOptsFor": "Opciones Adicionales para {}",
|
||||||
|
"supportedSourcesBelow": "Fuentes Soportadas:",
|
||||||
|
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
||||||
|
"searchableInBrackets": "(Soporta Búsquedas)",
|
||||||
|
"appsString": "Aplicaciones",
|
||||||
|
"noApps": "Sin Aplicaciones",
|
||||||
|
"noAppsForFilter": "Sin Aplicaciones para Filtrar",
|
||||||
|
"byX": "Por {}",
|
||||||
|
"percentProgress": "Progreso: {}%",
|
||||||
|
"pleaseWait": "Por favor, espere",
|
||||||
|
"updateAvailable": "Actualización Disponible",
|
||||||
|
"estimateInBracketsShort": "(Aprox.)",
|
||||||
|
"notInstalled": "No Instalado",
|
||||||
|
"estimateInBrackets": "(Aproximado)",
|
||||||
|
"selectAll": "Seleccionar Todo",
|
||||||
|
"deselectN": "Deseleccionar {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
|
||||||
|
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
|
||||||
|
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
|
||||||
|
"updateX": "Actualizar {}",
|
||||||
|
"installX": "Instalar {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
|
||||||
|
"changeX": "Cambiar {}",
|
||||||
|
"installUpdateApps": "Instalar/Actualizar Aplicaciones",
|
||||||
|
"installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
|
||||||
|
"markXSelectedAppsAsUpdated": "¿Marcar {} Aplicaciones Seleccionadas como Actualizadas?",
|
||||||
|
"no": "No",
|
||||||
|
"yes": "Sí",
|
||||||
|
"markSelectedAppsUpdated": "Marcar Aplicaciones Seleccionadas como Actualizadas",
|
||||||
|
"pinToTop": "Fijar arriba",
|
||||||
|
"unpinFromTop": "Desfijar de arriba",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
|
||||||
|
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
|
||||||
|
"resetInstallStatus": "Restaurar Estado de Instalación",
|
||||||
|
"more": "Más",
|
||||||
|
"removeOutdatedFilter": "Elimiar Filtro de Aplicaciones Desactualizado",
|
||||||
|
"showOutdatedOnly": "Mostrar solo Aplicaciones Desactualizadas",
|
||||||
|
"filter": "Filtrar",
|
||||||
|
"filterActive": "Filtrar *",
|
||||||
|
"filterApps": "Filtrar Actualizaciones",
|
||||||
|
"appName": "Nombre de la Aplicación",
|
||||||
|
"author": "Autor",
|
||||||
|
"upToDateApps": "Aplicaciones Actualizadas",
|
||||||
|
"nonInstalledApps": "Aplicaciones No Instaladas",
|
||||||
|
"importExport": "Importar/Exportar",
|
||||||
|
"settings": "Ajustes",
|
||||||
|
"exportedTo": "Exportado a {}",
|
||||||
|
"obtainiumExport": "Exportar Obtainium",
|
||||||
|
"invalidInput": "Input incorrecto",
|
||||||
|
"importedX": "Importado {}",
|
||||||
|
"obtainiumImport": "Importar Obtainium",
|
||||||
|
"importFromURLList": "Importar desde lista de URLs",
|
||||||
|
"searchQuery": "Consulta de Búsqueda",
|
||||||
|
"appURLList": "Lista de URLs de Aplicaciones",
|
||||||
|
"line": "Línea",
|
||||||
|
"searchX": "Buscar {}",
|
||||||
|
"noResults": "Resultados no encontrados",
|
||||||
|
"importX": "Importar {}",
|
||||||
|
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
|
||||||
|
"importErrors": "Import Errors",
|
||||||
|
"importedXOfYApps": "{} de {} Aplicaciones importadas.",
|
||||||
|
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
|
||||||
|
"okay": "Correcto",
|
||||||
|
"selectURL": "Seleccionar URL",
|
||||||
|
"selectURLs": "Seleccionar URLs",
|
||||||
|
"pick": "Escoger",
|
||||||
|
"theme": "Tema",
|
||||||
|
"dark": "Oscuro",
|
||||||
|
"light": "Claro",
|
||||||
|
"followSystem": "Seguir al Sistema",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Usar tema oscuro con negros puros",
|
||||||
|
"appSortBy": "Ordenar Aplicaciones Por",
|
||||||
|
"authorName": "Autor/Nombre",
|
||||||
|
"nameAuthor": "Nombre/Autor",
|
||||||
|
"asAdded": "Según se Añadieron",
|
||||||
|
"appSortOrder": "Orden de Clasificación de Aplicaciones",
|
||||||
|
"ascending": "Ascendente",
|
||||||
|
"descending": "Descendente",
|
||||||
|
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
|
||||||
|
"neverManualOnly": "Nunca - Solo Manual",
|
||||||
|
"appearance": "Apariencia",
|
||||||
|
"showWebInAppView": "Mostrar Vista de la Web de Origen",
|
||||||
|
"pinUpdates": "Fijar Actualizaciones en la Parte Superior de la Vista de Aplicaciones",
|
||||||
|
"updates": "Actualizaciones",
|
||||||
|
"sourceSpecific": "Fuente Específica",
|
||||||
|
"appSource": "Fuente de la Aplicación",
|
||||||
|
"noLogs": "Sin Logs",
|
||||||
|
"appLogs": "Logs de la Aplicación",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"share": "Compartir",
|
||||||
|
"appNotFound": "Aplicación no encontrada",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
|
"pickAnAPK": "Elige una APK",
|
||||||
|
"appHasMoreThanOnePackage": "{} tiene más de un paquete:",
|
||||||
|
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
|
||||||
|
"deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
|
||||||
|
"warning": "Aviso",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
|
||||||
|
"updatesAvailable": "Actualizaciones Disponibles",
|
||||||
|
"updatesAvailableNotifDescription": "Notifica al usuario de que hay actualizaciones para una o más aplicaciones monitorizadas por Obtainium",
|
||||||
|
"noNewUpdates": "No hay nuevas actualizaciones.",
|
||||||
|
"xHasAnUpdate": "{} tiene una actualización.",
|
||||||
|
"appsUpdated": "Aplicaciones Actualizadas",
|
||||||
|
"appsUpdatedNotifDescription": "Notifica al usuario de que una o más aplicaciones han sido actualizadas en segundo plano",
|
||||||
|
"xWasUpdatedToY": "{} ha sido actualizada a {}.",
|
||||||
|
"errorCheckingUpdates": "Error Buscando Actualizaciones",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "Una notificación que muestra cuándo la comprobación de actualizaciones en segundo plano falla",
|
||||||
|
"appsRemoved": "Aplicaciones Eliminadas",
|
||||||
|
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
|
||||||
|
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
|
||||||
|
"completeAppInstallation": "Instalación Completa de la Aplicación",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
|
||||||
|
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
|
||||||
|
"checkingForUpdates": "Buscando Actualizaciones",
|
||||||
|
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
|
||||||
|
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
|
||||||
|
"trackOnly": "Solo Seguimiento",
|
||||||
|
"errorWithHttpStatusCode": "Error {}",
|
||||||
|
"versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
|
||||||
|
"unknown": "Desconocido",
|
||||||
|
"none": "Ninguno",
|
||||||
|
"never": "Nunca",
|
||||||
|
"latestVersionX": "Última Versión: {}",
|
||||||
|
"installedVersionX": "Versión Instalada: {}",
|
||||||
|
"lastUpdateCheckX": "Última Comprobación: {}",
|
||||||
|
"remove": "Eliminar",
|
||||||
|
"yesMarkUpdated": "Sí, Marcar como Actualizada",
|
||||||
|
"fdroid": "Repositorio oficial de F-Droid",
|
||||||
|
"appIdOrName": "ID o Nombre de la Aplicación",
|
||||||
|
"appId": "ID de la Aplicación",
|
||||||
|
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
|
||||||
|
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
|
||||||
|
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Instalar",
|
||||||
|
"markInstalled": "Marcar como Instalda",
|
||||||
|
"update": "Actualizar",
|
||||||
|
"markUpdated": "Marcar como Actualizada",
|
||||||
|
"additionalOptions": "Opciones Adicionales",
|
||||||
|
"disableVersionDetection": "Descativar Detección de Versiones",
|
||||||
|
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
|
||||||
|
"downloadingX": "Descargando {}",
|
||||||
|
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
|
||||||
|
"noAPKFound": "APK no encontrada",
|
||||||
|
"noVersionDetection": "Sin detección de versiones",
|
||||||
|
"categorize": "Catogorizar",
|
||||||
|
"categories": "Categorías",
|
||||||
|
"category": "Categoría",
|
||||||
|
"noCategory": "Sin Categoría",
|
||||||
|
"noCategories": "Sin Categorías",
|
||||||
|
"deleteCategoriesQuestion": "¿Borrar Categorías?",
|
||||||
|
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
|
||||||
|
"addCategory": "Añadir Categoría",
|
||||||
|
"label": "Nombre",
|
||||||
|
"language": "Idioma",
|
||||||
|
"copiedToClipboard": "Copiado al Portapapeles",
|
||||||
|
"storagePermissionDenied": "Permiso de Almacenamiento rechazado",
|
||||||
|
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
|
||||||
|
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
|
||||||
|
"removeFromObtainium": "Eliminar de Obtainium",
|
||||||
|
"uninstallFromDevice": "Desinstalar del Dispositivo",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
|
||||||
|
"releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
|
||||||
|
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
|
||||||
|
"changes": "Cambios",
|
||||||
|
"releaseDate": "Fecha de Publicación",
|
||||||
|
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
|
||||||
|
"versionDetection": "Detección de Versiones",
|
||||||
|
"standardVersionDetection": "Detección de versiones estándar",
|
||||||
|
"groupByCategory": "Agrupar por Categoría",
|
||||||
|
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
|
||||||
|
"overrideSource": "Sobrescribir Fuente",
|
||||||
|
"dontShowAgain": "No mostrar de nuevo",
|
||||||
|
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
||||||
|
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
|
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "¿Eliminar Aplicación?",
|
||||||
|
"other": "¿Eliminar Aplicaciones?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Muchas peticiones (limitado) - prueba de nuevo en {} minuto",
|
||||||
|
"other": "Muchas peticiones (limitado) - prueba de nuevo en {} minutos"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minuto",
|
||||||
|
"other": "La comprobación de actualizaciones en segundo plano se ha encontrado un {}, se volverá a probar en {} minutos"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualización - se notificará al usuario si es necesario",
|
||||||
|
"other": "La comprobación de actualizaciones en segundo plano ha encontrado {} actualizaciones - se notificará al usuario si es necesario"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} Aplicación",
|
||||||
|
"other": "{} Aplicaciones"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} URL",
|
||||||
|
"other": "{} URLs"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} Minuto",
|
||||||
|
"other": "{} Minutos"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} Hora",
|
||||||
|
"other": "{} Horas"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} Día",
|
||||||
|
"other": "{} Días"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
|
||||||
|
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} y 1 aplicación más tiene actualizaciones.",
|
||||||
|
"other": "{} y {} aplicaciones más tiene actualizaciones."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} y 1 aplicación más han sido actualizadas.",
|
||||||
|
"other": "{} y {} aplicaciones más han sido actualizadas."
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
||||||
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "درباره گیتهاب PATs",
|
|
||||||
"includePrereleases": "شامل نسخه های اولیه",
|
"includePrereleases": "شامل نسخه های اولیه",
|
||||||
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
||||||
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"followSystem": "هماهنگ با سیستم",
|
"followSystem": "هماهنگ با سیستم",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "استفاده از تم تیره سیاه خالص",
|
||||||
"appSortBy": "مرتب سازی برنامه بر اساس",
|
"appSortBy": "مرتب سازی برنامه بر اساس",
|
||||||
"authorName": "سازنده/اسم",
|
"authorName": "سازنده/اسم",
|
||||||
"nameAuthor": "اسم/سازنده",
|
"nameAuthor": "اسم/سازنده",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}",
|
"lastUpdateCheckX": "بررسی آخرین بهروزرسانی: {}",
|
||||||
"remove": "حذف",
|
"remove": "حذف",
|
||||||
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
"yesMarkUpdated": "بله، علامت گذاری به عنوان به روز شده",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "شناسه یا نام برنامه",
|
"appIdOrName": "شناسه یا نام برنامه",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
"appWithIdOrNameNotFound": "هیچ برنامه ای با آن شناسه یا نام یافت نشد",
|
||||||
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
"reposHaveMultipleApps": "مخازن ممکن است شامل چندین برنامه باشد",
|
||||||
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
"fdroidThirdPartyRepo": "مخازن شخص ثالث F-Droid",
|
||||||
@ -207,6 +208,7 @@
|
|||||||
"addCategory": "اضافه کردن دسته",
|
"addCategory": "اضافه کردن دسته",
|
||||||
"label": "برچسب",
|
"label": "برچسب",
|
||||||
"language": "زبان",
|
"language": "زبان",
|
||||||
|
"copiedToClipboard": "در کلیپ بورد کپی شد",
|
||||||
"storagePermissionDenied": "مجوز ذخیره سازی رد شد",
|
"storagePermissionDenied": "مجوز ذخیره سازی رد شد",
|
||||||
"selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.",
|
"selectedCategorizeWarning": "این جایگزین تنظیمات دسته بندی موجود برای برنامه های انتخابی می شود.",
|
||||||
"filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید",
|
"filterAPKsByRegEx": "فایلهای APK را با نظم فیلتر کنید",
|
||||||
@ -220,7 +222,18 @@
|
|||||||
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
"importFromURLsInFile": "وارد کردن از آدرس های اینترنتی موجود در فایل (مانند OPML)",
|
||||||
"versionDetection": "تشخیص نسخه",
|
"versionDetection": "تشخیص نسخه",
|
||||||
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
"standardVersionDetection": "تشخیص نسخه استاندارد",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "گروه بر اساس دسته",
|
||||||
|
"autoApkFilterByArch": "در صورت امکان سعی کنید APKها را بر اساس معماری CPU فیلتر کنید",
|
||||||
|
"overrideSource": "نادیده گرفتن منبع",
|
||||||
|
"dontShowAgain": "دوباره این را نشان نده",
|
||||||
|
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||||
|
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||||
|
"moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
|
||||||
|
"gitlabPATLabel": "رمز دسترسی شخصی GitLab (جستجو را فعال می کند)",
|
||||||
|
"about": "درباره",
|
||||||
|
"requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
|
||||||
|
"checkOnStart": "بررسی در شروع",
|
||||||
|
"tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "برنامه حذف شود؟",
|
"one": "برنامه حذف شود؟",
|
||||||
"other": "برنامه ها حذف شوند؟"
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
|
"githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
|
||||||
"githubPATHint": "Le JAP doit être dans ce format : username:token",
|
"githubPATHint": "Le JAP doit être dans ce format : username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "À propos des JAP GitHub",
|
|
||||||
"includePrereleases": "Inclure les avant-premières",
|
"includePrereleases": "Inclure les avant-premières",
|
||||||
"fallbackToOlderReleases": "Retour aux anciennes versions",
|
"fallbackToOlderReleases": "Retour aux anciennes versions",
|
||||||
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
|
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"followSystem": "Suivre le système",
|
"followSystem": "Suivre le système",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "Applications triées par",
|
"appSortBy": "Applications triées par",
|
||||||
"authorName": "Auteur/Nom",
|
"authorName": "Auteur/Nom",
|
||||||
"nameAuthor": "Nom/Auteur",
|
"nameAuthor": "Nom/Auteur",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
|
"lastUpdateCheckX": "Vérification de la dernière mise à jour : {}",
|
||||||
"remove": "Retirer",
|
"remove": "Retirer",
|
||||||
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
"yesMarkUpdated": "Oui, marquer comme mis à jour",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "ID ou nom de l'application",
|
"appIdOrName": "ID ou nom de l'application",
|
||||||
|
"appId": "ID de l'application",
|
||||||
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
"appWithIdOrNameNotFound": "Aucune application n'a été trouvée avec cet identifiant ou ce nom",
|
||||||
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
"reposHaveMultipleApps": "Les dépôts peuvent contenir plusieurs applications",
|
||||||
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
"fdroidThirdPartyRepo": "Dépôt tiers F-Droid",
|
||||||
@ -207,6 +208,7 @@
|
|||||||
"addCategory": "Ajouter une catégorie",
|
"addCategory": "Ajouter une catégorie",
|
||||||
"label": "Étiquette",
|
"label": "Étiquette",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
|
"copiedToClipboard": "Copied to Clipboard",
|
||||||
"storagePermissionDenied": "Autorisation de stockage refusée",
|
"storagePermissionDenied": "Autorisation de stockage refusée",
|
||||||
"selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
|
"selectedCategorizeWarning": "Cela remplacera tous les paramètres de catégorie existants pour les applications sélectionnées.",
|
||||||
"filterAPKsByRegEx": "Filtrer les APK par expression régulière",
|
"filterAPKsByRegEx": "Filtrer les APK par expression régulière",
|
||||||
@ -221,6 +223,17 @@
|
|||||||
"versionDetection": "Détection des versions",
|
"versionDetection": "Détection des versions",
|
||||||
"standardVersionDetection": "Détection de version standard",
|
"standardVersionDetection": "Détection de version standard",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "Group by Category",
|
||||||
|
"autoApkFilterByArch": "Attempt to filter APKs by CPU architecture if possible",
|
||||||
|
"overrideSource": "Override Source",
|
||||||
|
"dontShowAgain": "Don't show this again",
|
||||||
|
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||||
|
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
||||||
|
"moveNonInstalledAppsToBottom": "Move Non-Installed Apps to Bottom of Apps View",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Enables Search)",
|
||||||
|
"about": "About",
|
||||||
|
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
|
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Supprimer l'application ?",
|
"one": "Supprimer l'application ?",
|
||||||
"other": "Supprimer les applications ?"
|
"other": "Supprimer les applications ?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||||
"githubPATFormat": "felhasználónév:token",
|
"githubPATFormat": "felhasználónév:token",
|
||||||
"githubPATLinkText": "A GitHub PAT-okról",
|
|
||||||
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
|
||||||
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
|
||||||
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"followSystem": "Rendszer szerint",
|
"followSystem": "Rendszer szerint",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Használjon tiszta fekete sötét témát",
|
||||||
"appSortBy": "App rendezés...",
|
"appSortBy": "App rendezés...",
|
||||||
"authorName": "Szerző/Név",
|
"authorName": "Szerző/Név",
|
||||||
"nameAuthor": "Név/Szerző",
|
"nameAuthor": "Név/Szerző",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
"lastUpdateCheckX": "Frissítés ellenőrizve: {}",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
"yesMarkUpdated": "Igen, megjelölés frissítettként",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "App ID vagy név",
|
"appIdOrName": "App ID vagy név",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
"appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel",
|
||||||
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
"reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak",
|
||||||
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
"fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo",
|
||||||
@ -206,6 +207,7 @@
|
|||||||
"addCategory": "Új kategória",
|
"addCategory": "Új kategória",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
|
"copiedToClipboard": "Másolva a vágólapra",
|
||||||
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
"storagePermissionDenied": "Tárhely engedély megtagadva",
|
||||||
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
|
||||||
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
|
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
|
||||||
@ -219,7 +221,18 @@
|
|||||||
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
|
"importFromURLsInFile": "Importálás fájlban található URL-ből (mint pl. OPML)",
|
||||||
"versionDetection": "Verzió érzékelés",
|
"versionDetection": "Verzió érzékelés",
|
||||||
"standardVersionDetection": "Alapért. verzió érzékelés",
|
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "Csoportosítás Kategória alapján",
|
||||||
|
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
||||||
|
"overrideSource": "Forrás felülbírálása",
|
||||||
|
"dontShowAgain": "Ne mutassa ezt újra",
|
||||||
|
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||||
|
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||||
|
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (Engedélyezi a Keresést)",
|
||||||
|
"about": "Rólunk",
|
||||||
|
"requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
|
||||||
|
"checkOnStart": "Egyszer az indításkor",
|
||||||
|
"tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Eltávolítja az alkalmazást?",
|
"one": "Eltávolítja az alkalmazást?",
|
||||||
"other": "Eltávolítja az alkalmazást?"
|
"other": "Eltávolítja az alkalmazást?"
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
{
|
{
|
||||||
"invalidURLForSource": "URL dell'App da {} non valido",
|
"invalidURLForSource": "URL dell'app {} non valido",
|
||||||
"noReleaseFound": "Impossibile trovare una release adatta",
|
"noReleaseFound": "Impossibile trovare una release adatta",
|
||||||
"noVersionFound": "Impossibile determinare la versione della release",
|
"noVersionFound": "Impossibile determinare la versione della release",
|
||||||
"urlMatchesNoSource": "L'URL non corrisponde ad alcuna fonte conosciuta",
|
"urlMatchesNoSource": "L'URL non corrisponde ad alcuna fonte conosciuta",
|
||||||
"cantInstallOlderVersion": "Impossibile installare una versione precedente di un'App",
|
"cantInstallOlderVersion": "Impossibile installare una versione precedente di un'app",
|
||||||
"appIdMismatch": "L'ID del pacchetto scaricato non corrisponde all'ID dell'App esistente",
|
"appIdMismatch": "L'ID del pacchetto scaricato non corrisponde all'ID dell'app esistente",
|
||||||
"functionNotImplemented": "Questa classe non ha implementato questa funzione",
|
"functionNotImplemented": "Questa classe non ha implementato questa funzione",
|
||||||
"placeholder": "Segnaposto",
|
"placeholder": "Segnaposto",
|
||||||
"someErrors": "Si sono verificati degli errori",
|
"someErrors": "Si sono verificati degli errori",
|
||||||
"unexpectedError": "Errore imprevisto",
|
"unexpectedError": "Errore imprevisto",
|
||||||
"ok": "Va bene",
|
"ok": "Va bene",
|
||||||
"and": "e",
|
"and": "e",
|
||||||
"startedBgUpdateTask": "Avviata l'attività di controllo degli aggiornamenti in background",
|
"startedBgUpdateTask": "Avviata l'attività di controllo degli aggiornamenti in secondo piano",
|
||||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
"bgUpdateIgnoreAfterIs": "Il parametro di agg. in secondo piano 'ignoreAfter' è {}",
|
||||||
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in background",
|
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in secondo piano",
|
||||||
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in background",
|
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in secondo piano",
|
||||||
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
||||||
"settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}",
|
"settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}",
|
||||||
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
||||||
"githubPATHint": "PAT deve seguire questo formato: username:token",
|
"githubPATHint": "PAT deve seguire questo formato: nomeutente:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "nomeutente:token",
|
||||||
"githubPATLinkText": "Informazioni su GitHub PAT",
|
|
||||||
"includePrereleases": "Includi prerelease",
|
"includePrereleases": "Includi prerelease",
|
||||||
"fallbackToOlderReleases": "Ripiega su release precedenti",
|
"fallbackToOlderReleases": "Ripiega su release precedenti",
|
||||||
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
|
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
|
||||||
@ -32,19 +31,19 @@
|
|||||||
"dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE",
|
"dropdownNoOptsError": "ERRORE: LA TENDINA DEVE AVERE ALMENO UN'OPZIONE",
|
||||||
"colour": "Colore",
|
"colour": "Colore",
|
||||||
"githubStarredRepos": "repository stellati da GitHub",
|
"githubStarredRepos": "repository stellati da GitHub",
|
||||||
"uname": "Username",
|
"uname": "Nome utente",
|
||||||
"wrongArgNum": "Numero di argomenti forniti errato",
|
"wrongArgNum": "Numero di argomenti forniti errato",
|
||||||
"xIsTrackOnly": "{} è in modalità Solo-Monitoraggio",
|
"xIsTrackOnly": "{} è in modalità Solo-Monitoraggio",
|
||||||
"source": "Fonte",
|
"source": "Fonte",
|
||||||
"app": "App",
|
"app": "App",
|
||||||
"appsFromSourceAreTrackOnly": "Le App da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
"appsFromSourceAreTrackOnly": "Le app da questa fonte sono in modalità 'Solo-Monitoraggio'.",
|
||||||
"youPickedTrackOnly": "È stata selezionata l'opzione 'Solo-Monitoraggio'.",
|
"youPickedTrackOnly": "È stata selezionata l'opzione 'Solo-Monitoraggio'.",
|
||||||
"trackOnlyAppDescription": "L'App sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
"trackOnlyAppDescription": "L'app sarà monitorata per gli aggiornamenti, ma Obtainium non sarà in grado di scaricarli o di installarli.",
|
||||||
"cancelled": "Annullato",
|
"cancelled": "Annullato",
|
||||||
"appAlreadyAdded": "App già aggiunta",
|
"appAlreadyAdded": "App già aggiunta",
|
||||||
"alreadyUpToDateQuestion": "L'App è già aggiornata?",
|
"alreadyUpToDateQuestion": "L'app è già aggiornata?",
|
||||||
"addApp": "Aggiungi App",
|
"addApp": "Aggiungi app",
|
||||||
"appSourceURL": "URL della fonte dell'App",
|
"appSourceURL": "URL della fonte dell'app",
|
||||||
"error": "Errore",
|
"error": "Errore",
|
||||||
"add": "Aggiungi",
|
"add": "Aggiungi",
|
||||||
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
|
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
|
||||||
@ -54,10 +53,10 @@
|
|||||||
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
||||||
"searchableInBrackets": "(ricercabile)",
|
"searchableInBrackets": "(ricercabile)",
|
||||||
"appsString": "App",
|
"appsString": "App",
|
||||||
"noApps": "Nessuna App",
|
"noApps": "Nessuna app",
|
||||||
"noAppsForFilter": "Nessuna App per i filtri selezionati",
|
"noAppsForFilter": "Nessuna app per i filtri selezionati",
|
||||||
"byX": "Di {}",
|
"byX": "Di {}",
|
||||||
"percentProgress": "Progresso: {}%",
|
"percentProgress": "Avanzamento: {}%",
|
||||||
"pleaseWait": "In attesa",
|
"pleaseWait": "In attesa",
|
||||||
"updateAvailable": "Aggiornamento disponibile",
|
"updateAvailable": "Aggiornamento disponibile",
|
||||||
"estimateInBracketsShort": "(prev.)",
|
"estimateInBracketsShort": "(prev.)",
|
||||||
@ -66,31 +65,31 @@
|
|||||||
"selectAll": "Seleziona tutto",
|
"selectAll": "Seleziona tutto",
|
||||||
"deselectN": "Deseleziona {}",
|
"deselectN": "Deseleziona {}",
|
||||||
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
|
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
|
||||||
"removeSelectedAppsQuestion": "Rimuovere le App selezionate?",
|
"removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
|
||||||
"removeSelectedApps": "Rimuovi le App selezionate",
|
"removeSelectedApps": "Rimuovi le app selezionate",
|
||||||
"updateX": "Aggiorna {}",
|
"updateX": "Aggiorna {}",
|
||||||
"installX": "Installa {}",
|
"installX": "Installa {}",
|
||||||
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato",
|
"markXTrackOnlyAsUpdated": "Contrassegna {}\n(Solo-Monitoraggio)\ncome aggiornato",
|
||||||
"changeX": "Modifica {}",
|
"changeX": "Modifica {}",
|
||||||
"installUpdateApps": "Installa/Aggiorna App",
|
"installUpdateApps": "Installa/Aggiorna app",
|
||||||
"installUpdateSelectedApps": "Installa/Aggiorna le App selezionate",
|
"installUpdateSelectedApps": "Installa/Aggiorna le app selezionate",
|
||||||
"markXSelectedAppsAsUpdated": "Contrassegnare le {} App selezionate come aggiornate?",
|
"markXSelectedAppsAsUpdated": "Contrassegnare le {} app selezionate come aggiornate?",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"yes": "Sì",
|
"yes": "Sì",
|
||||||
"markSelectedAppsUpdated": "Contrassegna le App selezionate come aggiornate",
|
"markSelectedAppsUpdated": "Contrassegna le app selezionate come aggiornate",
|
||||||
"pinToTop": "Fissa in alto",
|
"pinToTop": "Fissa in alto",
|
||||||
"unpinFromTop": "Rimuovi dall'alto",
|
"unpinFromTop": "Rimuovi dall'alto",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle App selezionate?",
|
"resetInstallStatusForSelectedAppsQuestion": "Ripristinare lo stato d'installazione delle app selezionate?",
|
||||||
"installStatusOfXWillBeResetExplanation": "Lo stato d'installazione di ogni App selezionata sarà ripristinato.\n\nCiò può essere d'aiuto nel caso in cui la versione mostrata dell'App in Obtainium non è corretta a causa di un aggiornamento fallito o di altri problemi.",
|
"installStatusOfXWillBeResetExplanation": "Lo stato d'installazione di ogni app selezionata sarà ripristinato.\n\nCiò può essere d'aiuto nel caso in cui la versione mostrata dell'app in Obtainium non sia corretta a causa di un aggiornamento fallito o di altri problemi.",
|
||||||
"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": "Altro",
|
||||||
"removeOutdatedFilter": "Rimuovi il filtro per le App non aggiornate",
|
"removeOutdatedFilter": "Rimuovi il filtro per le app non aggiornate",
|
||||||
"showOutdatedOnly": "Mostra solo le App non aggiornate",
|
"showOutdatedOnly": "Mostra solo le app non aggiornate",
|
||||||
"filter": "Filtri",
|
"filter": "Filtri",
|
||||||
"filterActive": "Filtri *",
|
"filterActive": "Filtri *",
|
||||||
"filterApps": "Filtra App",
|
"filterApps": "Filtra app",
|
||||||
"appName": "Nome dell'App",
|
"appName": "Nome dell'app",
|
||||||
"author": "Autore",
|
"author": "Autore",
|
||||||
"upToDateApps": "App aggiornate",
|
"upToDateApps": "App aggiornate",
|
||||||
"nonInstalledApps": "App non installate",
|
"nonInstalledApps": "App non installate",
|
||||||
@ -103,14 +102,14 @@
|
|||||||
"obtainiumImport": "Importa in Obtainium",
|
"obtainiumImport": "Importa in Obtainium",
|
||||||
"importFromURLList": "Importa da lista di URL",
|
"importFromURLList": "Importa da lista di URL",
|
||||||
"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 su {}",
|
"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.\nCiò non dovrebbe influire sui dati delle app.\n\nRiguarda solo l'URL e i metodi di importazione di terze parti.",
|
||||||
"importErrors": "Errori dell'importazione",
|
"importErrors": "Errori di importazione",
|
||||||
"importedXOfYApps": "{} App di {} importate.",
|
"importedXOfYApps": "{} app di {} importate.",
|
||||||
"followingURLsHadErrors": "I seguenti URL contengono errori:",
|
"followingURLsHadErrors": "I seguenti URL contengono errori:",
|
||||||
"okay": "Va bene",
|
"okay": "Va bene",
|
||||||
"selectURL": "Seleziona l'URL",
|
"selectURL": "Seleziona l'URL",
|
||||||
@ -119,26 +118,27 @@
|
|||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"dark": "Scuro",
|
"dark": "Scuro",
|
||||||
"light": "Chiaro",
|
"light": "Chiaro",
|
||||||
"followSystem": "Segui sistema",
|
"followSystem": "Segui il sistema",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Usa il tema Nero puro",
|
||||||
"appSortBy": "App ordinate per",
|
"appSortBy": "App ordinate per",
|
||||||
"authorName": "Autore/Nome",
|
"authorName": "Autore/Nome",
|
||||||
"nameAuthor": "Nome/Autore",
|
"nameAuthor": "Nome/Autore",
|
||||||
"asAdded": "Data di aggiunta",
|
"asAdded": "Data di aggiunta",
|
||||||
"appSortOrder": "Ordinamento",
|
"appSortOrder": "Ordine",
|
||||||
"ascending": "Ascendente",
|
"ascending": "Ascendente",
|
||||||
"descending": "Discendente",
|
"descending": "Discendente",
|
||||||
"bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in background",
|
"bgUpdateCheckInterval": "Intervallo di controllo degli aggiornamenti in secondo piano",
|
||||||
"neverManualOnly": "Mai - Solo manuale",
|
"neverManualOnly": "Mai - Solo manuale",
|
||||||
"appearance": "Aspetto",
|
"appearance": "Aspetto",
|
||||||
"showWebInAppView": "Mostra pagina web dell'App se selezionata",
|
"showWebInAppView": "Mostra pagina web dell'app se selezionata",
|
||||||
"pinUpdates": "Fissa aggiornamenti disponibili in alto",
|
"pinUpdates": "Fissa aggiornamenti disponibili in alto",
|
||||||
"updates": "Aggiornamenti",
|
"updates": "Aggiornamenti",
|
||||||
"sourceSpecific": "Specifiche per la fonte",
|
"sourceSpecific": "Specifiche per la fonte",
|
||||||
"appSource": "Sorgente dell'App",
|
"appSource": "Sorgente dell'app",
|
||||||
"noLogs": "Nessun log",
|
"noLogs": "Nessun log",
|
||||||
"appLogs": "Log dell'App",
|
"appLogs": "Log dell'app",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"share": "Condividi",
|
"share": "Condividi",
|
||||||
"appNotFound": "App non trovata",
|
"appNotFound": "App non trovata",
|
||||||
@ -148,28 +148,28 @@
|
|||||||
"deviceSupportsXArch": "Il dispositivo in uso supporta l'architettura {} della CPU.",
|
"deviceSupportsXArch": "Il dispositivo in uso supporta l'architettura {} della CPU.",
|
||||||
"deviceSupportsFollowingArchs": "Il dispositivo in uso supporta le seguenti architetture della CPU:",
|
"deviceSupportsFollowingArchs": "Il dispositivo in uso supporta le seguenti architetture della CPU:",
|
||||||
"warning": "Attenzione",
|
"warning": "Attenzione",
|
||||||
"sourceIsXButPackageFromYPrompt": "L'origine dell'App è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
"sourceIsXButPackageFromYPrompt": "L'origine dell'app è '{}' ma il pacchetto della release proviene da '{}'. Continuare?",
|
||||||
"updatesAvailable": "Aggiornamenti disponibili",
|
"updatesAvailable": "Aggiornamenti disponibili",
|
||||||
"updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più App monitorate da Obtainium",
|
"updatesAvailableNotifDescription": "Notifica all'utente che sono disponibili gli aggiornamenti di una o più app monitorate da Obtainium",
|
||||||
"noNewUpdates": "Nessun nuovo aggiornamento.",
|
"noNewUpdates": "Nessun nuovo aggiornamento.",
|
||||||
"xHasAnUpdate": "Aggiornamento disponibile per {}",
|
"xHasAnUpdate": "Aggiornamento disponibile per {}",
|
||||||
"appsUpdated": "App aggiornate",
|
"appsUpdated": "App aggiornate",
|
||||||
"appsUpdatedNotifDescription": "Notifica all'utente che una o più App sono state aggiornate in background",
|
"appsUpdatedNotifDescription": "Notifica all'utente che una o più app sono state aggiornate in secondo piano",
|
||||||
"xWasUpdatedToY": "{} è stato aggiornato a {}.",
|
"xWasUpdatedToY": "{} è stato aggiornato alla {}.",
|
||||||
"errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti",
|
"errorCheckingUpdates": "Controllo degli errori per gli aggiornamenti",
|
||||||
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in background fallisce",
|
"errorCheckingUpdatesNotifDescription": "Una notifica che mostra quando il controllo degli aggiornamenti in secondo piano fallisce",
|
||||||
"appsRemoved": "App rimosse",
|
"appsRemoved": "App rimosse",
|
||||||
"appsRemovedNotifDescription": "Notifica all'utente che una o più App sono state rimosse a causa di errori durante il caricamento",
|
"appsRemovedNotifDescription": "Notifica all'utente che una o più app sono state rimosse a causa di errori durante il caricamento",
|
||||||
"xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}",
|
"xWasRemovedDueToErrorY": "{} è stata rimosso a causa di questo errore: {}",
|
||||||
"completeAppInstallation": "Completa l'installazione dell'App",
|
"completeAppInstallation": "Completa l'installazione dell'app",
|
||||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le App",
|
"obtainiumMustBeOpenToInstallApps": "Obtainium deve essere aperto per poter installare le app",
|
||||||
"completeAppInstallationNotifDescription": "Chiede all'utente di riaprire Obtainium per terminare l'installazione di un App",
|
"completeAppInstallationNotifDescription": "Chiede all'utente di riaprire Obtainium per terminare l'installazione di un'app",
|
||||||
"checkingForUpdates": "Controllo degli aggiornamenti in corso",
|
"checkingForUpdates": "Controllo degli aggiornamenti in corso",
|
||||||
"checkingForUpdatesNotifDescription": "Notifica transitoria che appare durante la verifica degli aggiornamenti",
|
"checkingForUpdatesNotifDescription": "Notifica transitoria che appare durante la verifica degli aggiornamenti",
|
||||||
"pleaseAllowInstallPerm": "Per favore permetti a Obtainium di installare le App",
|
"pleaseAllowInstallPerm": "Per favore permetti a Obtainium di installare le app",
|
||||||
"trackOnly": "Solo-Monitoraggio",
|
"trackOnly": "Solo-Monitoraggio",
|
||||||
"errorWithHttpStatusCode": "Errore {}",
|
"errorWithHttpStatusCode": "Errore {}",
|
||||||
"versionCorrectionDisabled": "Correzione della versione disabilitata (il plugin non pare funzionare)",
|
"versionCorrectionDisabled": "Correzione della versione disattivata (il plugin sembra non funzionare)",
|
||||||
"unknown": "Sconosciuto",
|
"unknown": "Sconosciuto",
|
||||||
"none": "Nessuno",
|
"none": "Nessuno",
|
||||||
"never": "Mai",
|
"never": "Mai",
|
||||||
@ -178,10 +178,11 @@
|
|||||||
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
"lastUpdateCheckX": "Ultimo controllo degli aggiornamenti: {}",
|
||||||
"remove": "Rimuovi",
|
"remove": "Rimuovi",
|
||||||
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
"yesMarkUpdated": "Sì, contrassegna come aggiornato",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid ufficiale",
|
||||||
"appIdOrName": "ID o nome dell'App",
|
"appIdOrName": "ID o nome dell'app",
|
||||||
"appWithIdOrNameNotFound": "Non è stata trovata alcuna App con quell'ID o nome",
|
"appId": "ID dell'app",
|
||||||
"reposHaveMultipleApps": "I repository possono contenere più App",
|
"appWithIdOrNameNotFound": "Non è stata trovata alcuna app con quell'ID o nome",
|
||||||
|
"reposHaveMultipleApps": "I repository possono contenere più app",
|
||||||
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
"fdroidThirdPartyRepo": "Repository F-Droid di terze parti",
|
||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
@ -192,9 +193,9 @@
|
|||||||
"markUpdated": "Contrassegna come aggiornato",
|
"markUpdated": "Contrassegna come aggiornato",
|
||||||
"additionalOptions": "Opzioni aggiuntive",
|
"additionalOptions": "Opzioni aggiuntive",
|
||||||
"disableVersionDetection": "Disattiva il rilevamento della versione",
|
"disableVersionDetection": "Disattiva il rilevamento della versione",
|
||||||
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le App la cui versione non viene rilevata correttamente.",
|
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le app la cui versione non viene rilevata correttamente.",
|
||||||
"downloadingX": "Scaricamento di {} in corso",
|
"downloadingX": "Scaricamento di {} in corso",
|
||||||
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'App",
|
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'app",
|
||||||
"noAPKFound": "Nessun APK trovato",
|
"noAPKFound": "Nessun APK trovato",
|
||||||
"noVersionDetection": "Disattiva rilevamento di versione",
|
"noVersionDetection": "Disattiva rilevamento di versione",
|
||||||
"categorize": "Aggiungi a categoria",
|
"categorize": "Aggiungi a categoria",
|
||||||
@ -203,43 +204,55 @@
|
|||||||
"noCategory": "Nessuna categoria",
|
"noCategory": "Nessuna categoria",
|
||||||
"noCategories": "Nessuna categoria",
|
"noCategories": "Nessuna categoria",
|
||||||
"deleteCategoriesQuestion": "Eliminare le categorie?",
|
"deleteCategoriesQuestion": "Eliminare le categorie?",
|
||||||
"categoryDeleteWarning": "Tutte le App nelle categorie eliminate saranno impostate come non categorizzate.",
|
"categoryDeleteWarning": "Tutte le app nelle categorie eliminate saranno impostate come non categorizzate.",
|
||||||
"addCategory": "Aggiungi categoria",
|
"addCategory": "Aggiungi categoria",
|
||||||
"label": "Etichetta",
|
"label": "Etichetta",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
|
"copiedToClipboard": "Copiato negli appunti",
|
||||||
"storagePermissionDenied": "Accesso ai file non autorizzato",
|
"storagePermissionDenied": "Accesso ai file non autorizzato",
|
||||||
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le App selezionate.",
|
"selectedCategorizeWarning": "Ciò sostituirà le impostazioni di categoria esistenti per le app selezionate.",
|
||||||
"filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
|
"filterAPKsByRegEx": "Filtra file APK con espressioni regolari",
|
||||||
"removeFromObtainium": "Rimuovi da Obtainium",
|
"removeFromObtainium": "Rimuovi da Obtainium",
|
||||||
"uninstallFromDevice": "Disinstalla dal dispositivo",
|
"uninstallFromDevice": "Disinstalla dal dispositivo",
|
||||||
"onlyWorksWithNonVersionDetectApps": "Funziona solo per le App con il rilevamento della versione disattivato.",
|
"onlyWorksWithNonVersionDetectApps": "Funziona solo per le app con il rilevamento della versione disattivato.",
|
||||||
"releaseDateAsVersion": "Usa data di rilascio come versione",
|
"releaseDateAsVersion": "Usa data di rilascio come versione",
|
||||||
"releaseDateAsVersionExplanation": "Questa opzione dovrebbe essere usata solo per le App in cui il rilevamento della versione non funziona correttamente, ma è disponibile una data di rilascio.",
|
"releaseDateAsVersionExplanation": "Questa opzione dovrebbe essere usata solo per le app in cui il rilevamento della versione non funziona correttamente, ma è disponibile una data di rilascio.",
|
||||||
"changes": "Novità",
|
"changes": "Novità",
|
||||||
"releaseDate": "Data di rilascio",
|
"releaseDate": "Data di rilascio",
|
||||||
"importFromURLsInFile": "Importa da URL in file (come OPML)",
|
"importFromURLsInFile": "Importa da URL in file (come OPML)",
|
||||||
"versionDetection": "Rilevamento di versione",
|
"versionDetection": "Rilevamento di versione",
|
||||||
"standardVersionDetection": "Rilevamento di versione standard",
|
"standardVersionDetection": "Rilevamento di versione standard",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "Raggruppa per categoria",
|
||||||
|
"autoApkFilterByArch": "Tenta di filtrare gli APK in base all'architettura della CPU, se possibile",
|
||||||
|
"overrideSource": "Sovrascrivi fonte",
|
||||||
|
"dontShowAgain": "Non mostrarlo più",
|
||||||
|
"dontShowTrackOnlyWarnings": "Non mostrare gli avvisi 'Solo-Monitoraggio'",
|
||||||
|
"dontShowAPKOriginWarnings": "Non mostrare gli avvisi di origine dell'APK",
|
||||||
|
"moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
|
||||||
|
"gitlabPATLabel": "GitLab Personal Access Token (attiva la ricerca)",
|
||||||
|
"about": "Informazioni",
|
||||||
|
"requiresCredentialsInSettings": "Servono credenziali aggiuntive (in Impostazioni)",
|
||||||
|
"checkOnStart": "Controlla una volta all'avvio",
|
||||||
|
"tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'App?",
|
"one": "Rimuovere l'app?",
|
||||||
"other": "Rimuovere le App?"
|
"other": "Rimuovere le app?"
|
||||||
},
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
|
||||||
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"
|
||||||
},
|
},
|
||||||
"bgUpdateGotErrorRetryInMinutes": {
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
"one": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuto",
|
"one": "Il controllo degli aggiornamenti in secondo piano ha riscontrato un {}, nuovo tentativo tra {} minuto",
|
||||||
"other": "Il controllo degli aggiornamenti in background ha incontrato un {}, nuovo tentativo tra {} minuti"
|
"other": "Il controllo degli aggiornamenti in secondo piano ha riscontrato un {}, nuovo tentativo tra {} minuti"
|
||||||
},
|
},
|
||||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
"one": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamento - notificherà l'utente se necessario",
|
"one": "Il controllo degli aggiornamenti in secondo piano ha trovato {} aggiornamento - notificherà l'utente se necessario",
|
||||||
"other": "Il controllo degli aggiornamenti in background ha trovato {} aggiornamenti - notificherà l'utente se necessario"
|
"other": "Il controllo degli aggiornamenti in secondo piano ha trovato {} aggiornamenti - notificherà l'utente se necessario"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"one": "{} App",
|
"one": "{} app",
|
||||||
"other": "{} App"
|
"other": "{} app"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"one": "{} URL",
|
"one": "{} URL",
|
||||||
@ -258,15 +271,15 @@
|
|||||||
"other": "{} giorni"
|
"other": "{} giorni"
|
||||||
},
|
},
|
||||||
"clearedNLogsBeforeXAfterY": {
|
"clearedNLogsBeforeXAfterY": {
|
||||||
"one": "Pulito {n} log (prima = {before}, dopo = {after})",
|
"one": "Rimosso {n} log (prima = {before}, dopo = {after})",
|
||||||
"other": "Puliti {n} log (prima = {before}, dopo = {after})"
|
"other": "Rimossi {n} log (prima = {before}, dopo = {after})"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesAvailable": {
|
"xAndNMoreUpdatesAvailable": {
|
||||||
"one": "{} e un'altra App hanno aggiornamenti disponibili.",
|
"one": "{} e un'altra app hanno aggiornamenti disponibili.",
|
||||||
"other": "{} e altre {} App hanno aggiornamenti disponibili."
|
"other": "{} e altre {} app hanno aggiornamenti disponibili."
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesInstalled": {
|
"xAndNMoreUpdatesInstalled": {
|
||||||
"one": "{} e un'altra App sono state aggiornate.",
|
"one": "{} e un'altra app sono state aggiornate.",
|
||||||
"other": "{} e altre {} App sono state aggiornate."
|
"other": "{} e altre {} app sono state aggiornate."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||||
"githubPATFormat": "ユーザー名:トークン",
|
"githubPATFormat": "ユーザー名:トークン",
|
||||||
"githubPATLinkText": "GitHub PATsについて",
|
|
||||||
"includePrereleases": "プレリリースを含む",
|
"includePrereleases": "プレリリースを含む",
|
||||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||||
@ -122,6 +121,7 @@
|
|||||||
"followSystem": "システムに従う",
|
"followSystem": "システムに従う",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "ピュアブラックダークテーマを使用する",
|
||||||
"appSortBy": "アプリの並び方",
|
"appSortBy": "アプリの並び方",
|
||||||
"authorName": "作者名/アプリ名",
|
"authorName": "作者名/アプリ名",
|
||||||
"nameAuthor": "アプリ名/作者名",
|
"nameAuthor": "アプリ名/作者名",
|
||||||
@ -178,8 +178,9 @@
|
|||||||
"lastUpdateCheckX": "最終アップデート確認: {}",
|
"lastUpdateCheckX": "最終アップデート確認: {}",
|
||||||
"remove": "削除",
|
"remove": "削除",
|
||||||
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
"yesMarkUpdated": "はい、アップデート済みとしてマークします",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid Official",
|
||||||
"appIdOrName": "アプリのIDまたは名前",
|
"appIdOrName": "アプリのIDまたは名前",
|
||||||
|
"appId": "App ID",
|
||||||
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
"appWithIdOrNameNotFound": "そのIDや名前を持つアプリは見つかりませんでした",
|
||||||
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
"reposHaveMultipleApps": "リポジトリには複数のアプリが含まれることがあります",
|
||||||
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
"fdroidThirdPartyRepo": "F-Droid サードパーティリポジトリ",
|
||||||
@ -207,6 +208,7 @@
|
|||||||
"addCategory": "カテゴリを追加",
|
"addCategory": "カテゴリを追加",
|
||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
|
"copiedToClipboard": "クリップボードにコピーしました",
|
||||||
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||||
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||||
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
||||||
@ -220,7 +222,18 @@
|
|||||||
"importFromURLsInFile": "ファイル(OPMLなど)内のURLからインポート",
|
"importFromURLsInFile": "ファイル(OPMLなど)内のURLからインポート",
|
||||||
"versionDetection": "バージョン検出",
|
"versionDetection": "バージョン検出",
|
||||||
"standardVersionDetection": "標準のバージョン検出",
|
"standardVersionDetection": "標準のバージョン検出",
|
||||||
"groupByCategory": "Group by Category",
|
"groupByCategory": "カテゴリ別にグループ化する",
|
||||||
|
"autoApkFilterByArch": "可能であれば,CPUアーキテクチャによるAPKのフィルタリングを試みる",
|
||||||
|
"overrideSource": "ソースの上書き",
|
||||||
|
"dontShowAgain": "二度と表示しない",
|
||||||
|
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||||
|
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
|
||||||
|
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||||
|
"gitlabPATLabel": "GitLab パーソナルアクセストークン (検索を有効化する)",
|
||||||
|
"about": "概要",
|
||||||
|
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
|
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
@ -269,4 +282,4 @@
|
|||||||
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
||||||
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
285
assets/translations/pl.json
Normal file
285
assets/translations/pl.json
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"noDescription": "Brak opisu",
|
||||||
|
"no": "Nie",
|
||||||
|
"okay": "Okej",
|
||||||
|
"appId": "ID aplikacji",
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min.",
|
||||||
|
"other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min."
|
||||||
|
},
|
||||||
|
"invalidURLForSource": "Nieprawidłowy adres URL aplikacji {}",
|
||||||
|
"noReleaseFound": "Nie można znaleźć odpowiedniego wydania",
|
||||||
|
"noVersionFound": "Nie można określić wersji wydania",
|
||||||
|
"urlMatchesNoSource": "Adres URL nie pasuje do znanego źródła",
|
||||||
|
"cantInstallOlderVersion": "Nie można zainstalować starszej wersji aplikacji",
|
||||||
|
"appIdMismatch": "Pobrany identyfikator pakietu nie pasuje do istniejącego identyfikatora aplikacji",
|
||||||
|
"functionNotImplemented": "Ta klasa nie zaimplementowała tej funkcji",
|
||||||
|
"placeholder": "Placeholder",
|
||||||
|
"someErrors": "Wystąpiły pewne błędy",
|
||||||
|
"unexpectedError": "Nieoczekiwany błąd",
|
||||||
|
"ok": "Okej",
|
||||||
|
"and": "i",
|
||||||
|
"startedBgUpdateTask": "Rozpoczęto zadanie sprawdzania aktualizacji w tle",
|
||||||
|
"bgUpdateIgnoreAfterIs": "Parametr ignoreAfter aktualizacji w tle to {}",
|
||||||
|
"startedActualBGUpdateCheck": "Rozpoczęto sprawdzanie aktualizacji w tle",
|
||||||
|
"bgUpdateTaskFinished": "Zakończono zadanie sprawdzania aktualizacji w tle",
|
||||||
|
"firstRun": "Jest to pierwsze uruchomienie Obtainium",
|
||||||
|
"settingUpdateCheckIntervalTo": "Ustawianie interwału aktualizacji na {}",
|
||||||
|
"githubPATLabel": "Osobisty Token Dostępu GitHub (zwiększa limit zapytań)",
|
||||||
|
"githubPATHint": "Wymagany format OTD: użytkownik:token",
|
||||||
|
"githubPATFormat": "użytkownik:token",
|
||||||
|
"includePrereleases": "Uwzględnij wersje wstępne",
|
||||||
|
"fallbackToOlderReleases": "Powracaj do starszych wersji",
|
||||||
|
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
|
||||||
|
"invalidRegEx": "Nieprawidłowe wyrażenie regularne",
|
||||||
|
"cancel": "Anuluj",
|
||||||
|
"continue": "Kontynuuj",
|
||||||
|
"requiredInBrackets": "(Wymagane)",
|
||||||
|
"dropdownNoOptsError": "BŁĄD: LISTA ROZWIJANA MUSI MIEĆ CO NAJMNIEJ JEDNĄ OPCJĘ",
|
||||||
|
"colour": "Kolor",
|
||||||
|
"githubStarredRepos": "Repozytoria GitHub oznaczone gwiazdką",
|
||||||
|
"uname": "Nazwa użytkownika",
|
||||||
|
"wrongArgNum": "Nieprawidłowa liczba podanych argumentów",
|
||||||
|
"xIsTrackOnly": "{} jest tylko obserwowana",
|
||||||
|
"source": "Źródło",
|
||||||
|
"app": "Aplikacja",
|
||||||
|
"appsFromSourceAreTrackOnly": "Aplikacje z tego źródła są „Obserwowane”.",
|
||||||
|
"youPickedTrackOnly": "Wybrano opcję „Tylko obserwuj”.",
|
||||||
|
"trackOnlyAppDescription": "Aplikacja będzie obserwowana pod kątem aktualizacji, ale Obtainium nie będzie w stanie jej pobrać ani zainstalować.",
|
||||||
|
"cancelled": "Anulowano",
|
||||||
|
"appAlreadyAdded": "Aplikacja już została dodana",
|
||||||
|
"alreadyUpToDateQuestion": "Aplikacja jest już aktualna?",
|
||||||
|
"addApp": "Dodaj apkę",
|
||||||
|
"appSourceURL": "Adres URL źródła aplikacji",
|
||||||
|
"error": "Błąd",
|
||||||
|
"add": "Dodaj",
|
||||||
|
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
|
||||||
|
"search": "Szukaj",
|
||||||
|
"additionalOptsFor": "Dodatkowe opcje dla {}",
|
||||||
|
"supportedSourcesBelow": "Obsługiwane źródła:",
|
||||||
|
"trackOnlyInBrackets": "(tylko obserwowane)",
|
||||||
|
"searchableInBrackets": "(Wyszukiwalne)",
|
||||||
|
"appsString": "Aplikacje",
|
||||||
|
"noApps": "Brak aplikacji",
|
||||||
|
"noAppsForFilter": "Brak aplikacji dla filtra",
|
||||||
|
"byX": "Autorstwa {}",
|
||||||
|
"percentProgress": "Postęp: {}%",
|
||||||
|
"pleaseWait": "Proszę czekać",
|
||||||
|
"updateAvailable": "Dostępna aktualizacja",
|
||||||
|
"estimateInBracketsShort": "(Szac.)",
|
||||||
|
"notInstalled": "Nie zainstalowano",
|
||||||
|
"estimateInBrackets": "(Szacunkowo)",
|
||||||
|
"selectAll": "Zaznacz wszystkie",
|
||||||
|
"deselectN": "Odznacz {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
|
||||||
|
"removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
|
||||||
|
"removeSelectedApps": "Usuń wybrane aplikacje",
|
||||||
|
"updateX": "Zaktualizuj {}",
|
||||||
|
"installX": "Zainstaluj {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Oznacz {}\n(Tylko obserwowana)\njako zaktualizowaną",
|
||||||
|
"changeX": "Zmień {}",
|
||||||
|
"installUpdateApps": "Instaluj/aktualizuj aplikacje",
|
||||||
|
"installUpdateSelectedApps": "Zainstaluj/zaktualizuj wybrane aplikacje",
|
||||||
|
"markXSelectedAppsAsUpdated": "Oznaczyć {} wybranych aplikacji jako zaktualizowane?",
|
||||||
|
"yes": "Tak",
|
||||||
|
"markSelectedAppsUpdated": "Oznacz wybrane aplikacje jako zaktualizowane",
|
||||||
|
"pinToTop": "Przypnij",
|
||||||
|
"unpinFromTop": "Odepnij",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "Zresetować status instalacji dla wybranych aplikacji?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "Stan instalacji wybranych aplikacji zostanie zresetowany.\n\nMoże być to pomocne, gdy wersja aplikacji wyświetlana w Obtainium jest nieprawidłowa z powodu nieudanych aktualizacji lub innych problemów.",
|
||||||
|
"shareSelectedAppURLs": "Udostępnij wybrane adresy URL aplikacji",
|
||||||
|
"resetInstallStatus": "Zresetuj stan instalacji",
|
||||||
|
"more": "Więcej",
|
||||||
|
"removeOutdatedFilter": "Usuń filtr nieaktualnych aplikacji",
|
||||||
|
"showOutdatedOnly": "Pokaż tylko nieaktualne aplikacje",
|
||||||
|
"filter": "FIltr",
|
||||||
|
"filterActive": "Filtruj *",
|
||||||
|
"filterApps": "Filtruj aplikacje",
|
||||||
|
"appName": "Nazwa aplikacji",
|
||||||
|
"author": "Autor",
|
||||||
|
"upToDateApps": "Aktualne aplikacje",
|
||||||
|
"nonInstalledApps": "Niezainstalowane aplikacje",
|
||||||
|
"importExport": "Import/Eksport",
|
||||||
|
"settings": "Ustawienia",
|
||||||
|
"exportedTo": "Wyeksportowano do {}",
|
||||||
|
"obtainiumExport": "Eksportuj Obtainium",
|
||||||
|
"invalidInput": "Nieprawidłowe wprowadzenie",
|
||||||
|
"importedX": "Zaimportowano {}",
|
||||||
|
"obtainiumImport": "Import Obtainium",
|
||||||
|
"importFromURLList": "Importuj z listy adresów URL",
|
||||||
|
"searchQuery": "Wyszukiwane zapytanie",
|
||||||
|
"appURLList": "Lista adresów URL aplikacji",
|
||||||
|
"line": "Linia",
|
||||||
|
"searchX": "Przeszukaj {}",
|
||||||
|
"noResults": "Nie znaleziono wyników",
|
||||||
|
"importX": "Importuj {}",
|
||||||
|
"importedAppsIdDisclaimer": "Zaimportowane aplikacje mogą być wyświetlane jako „Niezainstalowane”.\nAby to naprawić, zainstaluj je ponownie za pomocą Obtainium.\nNie powinno to mieć wpływu na dane aplikacji.\n\nDotyczy tylko adresów URL i metod importu innych aplikacji.",
|
||||||
|
"importErrors": "Błędy importowania",
|
||||||
|
"importedXOfYApps": "Zaimportowano {} z {} aplikacji.",
|
||||||
|
"followingURLsHadErrors": "Następujące adresy URL zawierały błędy:",
|
||||||
|
"selectURL": "Wybierz adres URL",
|
||||||
|
"selectURLs": "Wybierz adresy URL",
|
||||||
|
"pick": "Wybierz",
|
||||||
|
"theme": "Motyw",
|
||||||
|
"dark": "Ciemny",
|
||||||
|
"light": "Jasny",
|
||||||
|
"followSystem": "Zgodny z systemem",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Użyj czarnego motywu",
|
||||||
|
"appSortBy": "Sortuj aplikacje według",
|
||||||
|
"authorName": "Autor/Nazwa",
|
||||||
|
"nameAuthor": "Nazwa/Autor",
|
||||||
|
"asAdded": "Dodania",
|
||||||
|
"appSortOrder": "Kolejność sortowania aplikacji",
|
||||||
|
"ascending": "Rosnąco",
|
||||||
|
"descending": "Malejąco",
|
||||||
|
"bgUpdateCheckInterval": "Częstotliwość sprawdzania aktualizacji w tle",
|
||||||
|
"neverManualOnly": "Nigdy - tylko ręcznie",
|
||||||
|
"appearance": "Wygląd",
|
||||||
|
"showWebInAppView": "Pokaż stronę źródłową w widoku aplikacji",
|
||||||
|
"pinUpdates": "Przypnij aktualizacje na górze widoku aplikacji",
|
||||||
|
"updates": "Aktualizacje",
|
||||||
|
"sourceSpecific": "Zależnie od źródła",
|
||||||
|
"appSource": "Źródło aplikacji",
|
||||||
|
"noLogs": "Brak logów",
|
||||||
|
"appLogs": "Logi aplikacji",
|
||||||
|
"close": "Zamknij",
|
||||||
|
"share": "Udostępnij",
|
||||||
|
"appNotFound": "Nie znaleziono aplikacji",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-eksport",
|
||||||
|
"pickAnAPK": "Wybierz plik APK",
|
||||||
|
"appHasMoreThanOnePackage": "{} ma więcej niż jeden pakiet:",
|
||||||
|
"deviceSupportsXArch": "Urządzenie obsługuje architekturę procesora {}.",
|
||||||
|
"deviceSupportsFollowingArchs": "Urządzenie obsługuje następujące architektury procesora:",
|
||||||
|
"warning": "Uwaga",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "Źródłem aplikacji jest '{}', ale pakiet wydania pochodzi z '{}'. Kontynuować?",
|
||||||
|
"updatesAvailable": "Dostępne aktualizacje",
|
||||||
|
"updatesAvailableNotifDescription": "Powiadamia użytkownika o dostępności aktualizacji dla jednej lub więcej aplikacji obserwowanych przez Obtainium",
|
||||||
|
"noNewUpdates": "Brak nowych aktualizacji.",
|
||||||
|
"xHasAnUpdate": "{} ma aktualizację.",
|
||||||
|
"appsUpdated": "Zaktualizowane aplikacje",
|
||||||
|
"appsUpdatedNotifDescription": "Powiadamia użytkownika, gdy jedna lub więcej aplikacji zostało zaktualizowanych w tle",
|
||||||
|
"xWasUpdatedToY": "{} zaktualizowano do {}.",
|
||||||
|
"errorCheckingUpdates": "Sprawdzanie błędów aktualizacji",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "Powiadomienie wyświetlane, gdy sprawdzanie aktualizacji w tle nie powiedzie się",
|
||||||
|
"appsRemoved": "Usunięte aplikacje",
|
||||||
|
"appsRemovedNotifDescription": "Powiadamia użytkownika, gdy jedna lub więcej aplikacji zostało usuniętych z powodu błędów wczytywania",
|
||||||
|
"xWasRemovedDueToErrorY": "Usunięto {} z powodu błędu: {}",
|
||||||
|
"completeAppInstallation": "Ukończenie instalacji aplikacji",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Aby zainstalować aplikacje, Obtainium musi być otwarte",
|
||||||
|
"completeAppInstallationNotifDescription": "Prosi użytkownika o powrót do Obtainium w celu dokończenia instalacji aplikacji",
|
||||||
|
"checkingForUpdates": "Sprawdzanie aktualizacji",
|
||||||
|
"checkingForUpdatesNotifDescription": "Tymczasowe powiadomienie pojawiające się podczas sprawdzania aktualizacji",
|
||||||
|
"pleaseAllowInstallPerm": "Pozwól Obtainium instalować aplikacje",
|
||||||
|
"trackOnly": "Tylko obserwuj",
|
||||||
|
"errorWithHttpStatusCode": "Błąd {}",
|
||||||
|
"versionCorrectionDisabled": "Korekta wersji wyłączona (wtyczka wydaje się nie działać)",
|
||||||
|
"unknown": "Nieznane",
|
||||||
|
"none": "Brak",
|
||||||
|
"never": "Nigdy",
|
||||||
|
"latestVersionX": "Najnowsza wersja: {}",
|
||||||
|
"installedVersionX": "Zainstalowana wersja: {}",
|
||||||
|
"lastUpdateCheckX": "Ostatnio sprawdzono: {}",
|
||||||
|
"remove": "Usuń",
|
||||||
|
"yesMarkUpdated": "Tak, oznacz jako zaktualizowane",
|
||||||
|
"fdroid": "Oficjalny F-Droid",
|
||||||
|
"appIdOrName": "ID aplikacji lub nazwa",
|
||||||
|
"appWithIdOrNameNotFound": "Nie znaleziono aplikacji o tym identyfikatorze lub nazwie",
|
||||||
|
"reposHaveMultipleApps": "Repozytoria mogą zawierać wiele aplikacji",
|
||||||
|
"fdroidThirdPartyRepo": "Zewnętrzne repo F-Droid",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Mobilny Steam",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Instaluj",
|
||||||
|
"markInstalled": "Oznacz jako zainstalowane",
|
||||||
|
"update": "Zaktualizuj",
|
||||||
|
"markUpdated": "Oznacz jako zaktualizowane",
|
||||||
|
"additionalOptions": "Dodatkowe opcje",
|
||||||
|
"disableVersionDetection": "Wyłącz wykrywanie wersji",
|
||||||
|
"noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.",
|
||||||
|
"downloadingX": "Pobieranie {}",
|
||||||
|
"downloadNotifDescription": "Powiadamia użytkownika o postępach w pobieraniu aplikacji",
|
||||||
|
"noAPKFound": "Nie znaleziono pakietu APK",
|
||||||
|
"noVersionDetection": "Bez wykrywania wersji",
|
||||||
|
"categorize": "Kategoryzuj",
|
||||||
|
"categories": "Kategorie",
|
||||||
|
"category": "Kategoria",
|
||||||
|
"noCategory": "Bez kategorii",
|
||||||
|
"noCategories": "Brak kategorii",
|
||||||
|
"deleteCategoriesQuestion": "Usunąć kategorie?",
|
||||||
|
"categoryDeleteWarning": "Wszystkie aplikacje w usuniętych kategoriach zostaną ustawione jako nieskategoryzowane.",
|
||||||
|
"addCategory": "Dodaj kategorię",
|
||||||
|
"label": "Etykieta",
|
||||||
|
"language": "Język",
|
||||||
|
"copiedToClipboard": "Skopiowano do schowka",
|
||||||
|
"storagePermissionDenied": "Odmówiono zezwolenia dostępu do pamięci",
|
||||||
|
"selectedCategorizeWarning": "Spowoduje to zastąpienie wszystkich istniejących ustawień kategorii dla wybranych aplikacji.",
|
||||||
|
"filterAPKsByRegEx": "Filtruj pliki APK według wyrażeń regularnych",
|
||||||
|
"removeFromObtainium": "Usuń z Obtainium",
|
||||||
|
"uninstallFromDevice": "Odinstaluj z urządzenia",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Działa tylko w przypadku aplikacji z wyłączonym wykrywaniem wersji.",
|
||||||
|
"releaseDateAsVersion": "Użyj daty wydania jako wersji",
|
||||||
|
"releaseDateAsVersionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie, ale dostępna jest data wydania.",
|
||||||
|
"changes": "Zmiany",
|
||||||
|
"releaseDate": "Data wydania",
|
||||||
|
"importFromURLsInFile": "Importuj z adresów URL w pliku (typu OPML)",
|
||||||
|
"versionDetection": "Wykrywanie wersji",
|
||||||
|
"standardVersionDetection": "Standardowe wykrywanie wersji",
|
||||||
|
"groupByCategory": "Grupuj według kategorii",
|
||||||
|
"autoApkFilterByArch": "Spróbuj filtrować pliki APK według architektury procesora, jeśli to możliwe",
|
||||||
|
"overrideSource": "Nadpisz źródło",
|
||||||
|
"dontShowAgain": "Nie pokazuj tego ponownie",
|
||||||
|
"dontShowTrackOnlyWarnings": "Nie wyświetlaj ostrzeżeń „Tylko obserwowana”",
|
||||||
|
"dontShowAPKOriginWarnings": "Nie pokazuj ostrzeżeń o pochodzeniu APK",
|
||||||
|
"moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
|
||||||
|
"gitlabPATLabel": "Osobisty Token Dostępu GitLab (umożliwia wyszukiwanie)",
|
||||||
|
"about": "Więcej informacji",
|
||||||
|
"requiresCredentialsInSettings": "Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
|
||||||
|
"checkOnStart": "Sprawdź raz przy starcie",
|
||||||
|
"tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Usunąć aplikację?",
|
||||||
|
"other": "Usunąć aplikacje?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min.",
|
||||||
|
"other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min."
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "Podczas sprawdzania aktualizacji w tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||||
|
"other": "Podczas sprawdzania aktualizacji w tle znaleziono {} akt. - w razie potrzeby użytkownik zostanie o tym powiadomiony"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} aplik.",
|
||||||
|
"other": "{} aplik."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} adres URL",
|
||||||
|
"other": "{} adr. URL"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} min.",
|
||||||
|
"other": "{} min."
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} godz.",
|
||||||
|
"other": "{} godz."
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} dzień",
|
||||||
|
"other": "{} dni"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "Wyczyszczono {n} log (przed = {before}, po = {after})",
|
||||||
|
"other": "Wyczyszczono logi: {n} (przed = {before}, po = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "{} i jeszcze 1 aplikacja mają aktualizacje.",
|
||||||
|
"other": "{} i {} aplik. otrzymało aktualizacje."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "Zaktualizowano {} i jeszcze 1 aplikację.",
|
||||||
|
"other": "Zaktualizowano {} i {} aplik."
|
||||||
|
}
|
||||||
|
}
|
285
assets/translations/ru.json
Normal file
285
assets/translations/ru.json
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
{
|
||||||
|
"invalidURLForSource": "Неверный URL-адрес {} приложения",
|
||||||
|
"noReleaseFound": "Не удалось найти подходящий релиз",
|
||||||
|
"noVersionFound": "Не удалось определить версию релиза",
|
||||||
|
"urlMatchesNoSource": "URL-адрес не соответствует известному источнику",
|
||||||
|
"cantInstallOlderVersion": "Невозможно установить более старую версию приложения",
|
||||||
|
"appIdMismatch": "ID загруженного пакета не совпадает с существующим ID приложения",
|
||||||
|
"functionNotImplemented": "Этот класс не реализовал эту функцию",
|
||||||
|
"placeholder": "Заполнитель",
|
||||||
|
"someErrors": "Произошли некоторые ошибки",
|
||||||
|
"unexpectedError": "Неожиданная ошибка",
|
||||||
|
"ok": "Окей",
|
||||||
|
"and": "и",
|
||||||
|
"startedBgUpdateTask": "Запущена задача фоновой проверки обновлений",
|
||||||
|
"bgUpdateIgnoreAfterIs": "Параметр игнорирования фоновых обновлений: {}",
|
||||||
|
"startedActualBGUpdateCheck": "Запущена фактическая проверка фоновых обновлений",
|
||||||
|
"bgUpdateTaskFinished": "Завершена задача фоновой проверки обновлений",
|
||||||
|
"firstRun": "Это первый запуск Obtainium",
|
||||||
|
"settingUpdateCheckIntervalTo": "Установка интервала проверки обновлений: {}",
|
||||||
|
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
|
||||||
|
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
|
||||||
|
"githubPATFormat": "имя_пользователя:токен",
|
||||||
|
"includePrereleases": "Включить предварительные релизы",
|
||||||
|
"fallbackToOlderReleases": "Откатиться к более старым версиям",
|
||||||
|
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов с помощью регулярного выражения",
|
||||||
|
"invalidRegEx": "Неверное регулярное выражение",
|
||||||
|
"noDescription": "Нет описания",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"continue": "Продолжить",
|
||||||
|
"requiredInBrackets": "(Обязательно)",
|
||||||
|
"dropdownNoOptsError": "Ошибка: Выпадающий список должен содержать хотя бы одну опцию",
|
||||||
|
"colour": "Цвет",
|
||||||
|
"githubStarredRepos": "Помеченные звездочкой репозитории на GitHub",
|
||||||
|
"uname": "Имя пользователя",
|
||||||
|
"wrongArgNum": "Неправильное количество предоставленных аргументов",
|
||||||
|
"xIsTrackOnly": "{} является приложением только для отслеживания",
|
||||||
|
"source": "Источник",
|
||||||
|
"app": "Приложение",
|
||||||
|
"appsFromSourceAreTrackOnly": "Приложения из этого источника являются 'только для отслеживания'.",
|
||||||
|
"youPickedTrackOnly": "Вы выбрали опцию 'Только для отслеживания'.",
|
||||||
|
"trackOnlyAppDescription": "Приложение будет отслеживаться на предмет обновлений, но Obtainium не сможет загрузить или установить его.",
|
||||||
|
"cancelled": "Отменено",
|
||||||
|
"appAlreadyAdded": "Приложение уже добавлено",
|
||||||
|
"alreadyUpToDateQuestion": "Приложение уже обновлено?",
|
||||||
|
"addApp": "Добавить приложение",
|
||||||
|
"appSourceURL": "URL-источник приложения",
|
||||||
|
"error": "Ошибка",
|
||||||
|
"add": "Добавить",
|
||||||
|
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
|
||||||
|
"search": "Поиск",
|
||||||
|
"additionalOptsFor": "Дополнительные опции для {}",
|
||||||
|
"supportedSourcesBelow": "Поддерживаемые источники:",
|
||||||
|
"trackOnlyInBrackets": "(Только для отслеживания)",
|
||||||
|
"searchableInBrackets": "(Поиск)",
|
||||||
|
"appsString": "Приложения",
|
||||||
|
"noApps": "Нет приложений",
|
||||||
|
"noAppsForFilter": "Нет приложений для фильтра",
|
||||||
|
"byX": "От {}",
|
||||||
|
"percentProgress": "Прогресс: {}%",
|
||||||
|
"pleaseWait": "Пожалуйста, подождите",
|
||||||
|
"updateAvailable": "Доступно обновление",
|
||||||
|
"estimateInBracketsShort": "(Оценка)",
|
||||||
|
"notInstalled": "Не установлено",
|
||||||
|
"estimateInBrackets": "(Оценка)",
|
||||||
|
"selectAll": "Выбрать все",
|
||||||
|
"deselectN": "Отменить выбор {}",
|
||||||
|
"xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется установленным на устройстве.",
|
||||||
|
"removeSelectedAppsQuestion": "Удалить выбранные приложения?",
|
||||||
|
"removeSelectedApps": "Удалить выбранные приложения",
|
||||||
|
"updateX": "Обновить {}",
|
||||||
|
"installX": "Установить {}",
|
||||||
|
"markXTrackOnlyAsUpdated": "Отметить {}\n(Только для отслеживания)\nкак обновленное",
|
||||||
|
"changeX": "Изменить {}",
|
||||||
|
"installUpdateApps": "Установить/Обновить приложения",
|
||||||
|
"installUpdateSelectedApps": "Установить/Обновить выбранные приложения",
|
||||||
|
"markXSelectedAppsAsUpdated": "Отметить {} выбранные приложения как обновленные?",
|
||||||
|
"no": "Нет",
|
||||||
|
"yes": "Да",
|
||||||
|
"markSelectedAppsUpdated": "Отметить выбранные приложения как обновленные",
|
||||||
|
"pinToTop": "Закрепить сверху",
|
||||||
|
"unpinFromTop": "Открепить",
|
||||||
|
"resetInstallStatusForSelectedAppsQuestion": "Сбросить статус установки для выбранных приложений?",
|
||||||
|
"installStatusOfXWillBeResetExplanation": "Статус установки для выбранных приложений будет сброшен.\n\nЭто может помочь, если версия приложения, отображаемая в Obtainium, неправильная из-за неудачных обновлений или других проблем.",
|
||||||
|
"shareSelectedAppURLs": "Поделиться выбранными URL-адресами приложений",
|
||||||
|
"resetInstallStatus": "Сбросить статус установки",
|
||||||
|
"more": "Еще",
|
||||||
|
"removeOutdatedFilter": "Удалить фильтр для устаревших приложений",
|
||||||
|
"showOutdatedOnly": "Показывать только устаревшие приложения",
|
||||||
|
"filter": "Фильтр",
|
||||||
|
"filterActive": "Фильтр *",
|
||||||
|
"filterApps": "Фильтровать приложения",
|
||||||
|
"appName": "Название приложения",
|
||||||
|
"author": "Автор",
|
||||||
|
"upToDateApps": "Приложения со свежими обновлениями",
|
||||||
|
"nonInstalledApps": "Неустановленные приложения",
|
||||||
|
"importExport": "Импорт/экспорт",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"exportedTo": "Экспортировано в {}",
|
||||||
|
"obtainiumExport": "Экспорт из Obtainium",
|
||||||
|
"invalidInput": "Неверный ввод",
|
||||||
|
"importedX": "Импортировано {}",
|
||||||
|
"obtainiumImport": "Импорт в Obtainium",
|
||||||
|
"importFromURLList": "Импорт из списка URL-адреса",
|
||||||
|
"searchQuery": "Поисковый запрос",
|
||||||
|
"appURLList": "Список URL приложений",
|
||||||
|
"line": "Строка",
|
||||||
|
"searchX": "Поиск {}",
|
||||||
|
"noResults": "Результатов не найдено",
|
||||||
|
"importX": "Импорт {}",
|
||||||
|
"importedAppsIdDisclaimer": "Импортированные приложения могут неверно отображаться как 'Не установлены'.\nДля исправления этой проблемы повторно установите их через Obtainium.\nЭто не должно повлиять на данные приложения.\n\nПроблемы возникают только при импорте из URL-адреса и сторонних источников.",
|
||||||
|
"importErrors": "Ошибка импорта",
|
||||||
|
"importedXOfYApps": "Импортировано {} из {} приложений.",
|
||||||
|
"followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
|
||||||
|
"okay": "Окей",
|
||||||
|
"selectURL": "Выбрать URL-адрес",
|
||||||
|
"selectURLs": "Выбрать URL-адреса",
|
||||||
|
"pick": "Выбрать",
|
||||||
|
"theme": "Тема",
|
||||||
|
"dark": "Темный",
|
||||||
|
"light": "Светлый",
|
||||||
|
"followSystem": "Следовать системе",
|
||||||
|
"obtainium": "Obtainium",
|
||||||
|
"materialYou": "Material You",
|
||||||
|
"useBlackTheme": "Использовать темную тему",
|
||||||
|
"appSortBy": "Сортировка приложений по",
|
||||||
|
"authorName": "Автор/Название",
|
||||||
|
"nameAuthor": "Название/Автор",
|
||||||
|
"asAdded": "В порядке добавления",
|
||||||
|
"appSortOrder": "Порядок сортировки приложений",
|
||||||
|
"ascending": "По возрастанию",
|
||||||
|
"descending": "По убыванию",
|
||||||
|
"bgUpdateCheckInterval": "Интервал проверки обновлений в фоновом режиме",
|
||||||
|
"neverManualOnly": "Никогда - Только вручную",
|
||||||
|
"appearance": "Внешний вид",
|
||||||
|
"showWebInAppView": "Показывать веб-страницу источника в представлении приложения",
|
||||||
|
"pinUpdates": "Закрепить обновления сверху списка приложений",
|
||||||
|
"updates": "Обновления",
|
||||||
|
"sourceSpecific": "Специфика источника",
|
||||||
|
"appSource": "Источник приложения",
|
||||||
|
"noLogs": "Нет журналов",
|
||||||
|
"appLogs": "Журналы приложений",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"share": "Поделиться",
|
||||||
|
"appNotFound": "Приложение не найдено",
|
||||||
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
|
"pickAnAPK": "Выберите APK-файл",
|
||||||
|
"appHasMoreThanOnePackage": "{} имеет более одного пакета:",
|
||||||
|
"deviceSupportsXArch": "Ваше устройство поддерживает архитектуру процессора {}.",
|
||||||
|
"deviceSupportsFollowingArchs": "Ваше устройство поддерживает следующие архитектуры процессора:",
|
||||||
|
"warning": "Предупреждение",
|
||||||
|
"sourceIsXButPackageFromYPrompt": "Источник приложения - '{}', но пакет для установки получен из '{}'. Продолжить?",
|
||||||
|
"updatesAvailable": "Доступны обновления",
|
||||||
|
"updatesAvailableNotifDescription": "Уведомляет пользователя о наличии обновлений для одного или нескольких приложений, отслеживаемых Obtainium",
|
||||||
|
"noNewUpdates": "Нет новых обновлений.",
|
||||||
|
"xHasAnUpdate": "{} есть обновление.",
|
||||||
|
"appsUpdated": "Приложения обновлены",
|
||||||
|
"appsUpdatedNotifDescription": "Уведомляет пользователя о том, что обновления для одного или нескольких приложений были применены в фоновом режиме",
|
||||||
|
"xWasUpdatedToY": "{} была обновлена до версии {}.",
|
||||||
|
"errorCheckingUpdates": "Ошибка при проверке обновлений",
|
||||||
|
"errorCheckingUpdatesNotifDescription": "Уведомление, которое появляется, когда проверка обновлений в фоновом режиме завершилась с ошибкой",
|
||||||
|
"appsRemoved": "Приложение удалено",
|
||||||
|
"appsRemovedNotifDescription": "Уведомляет пользователя о том, что одно или несколько приложений было удалено из-за ошибок при их загрузке",
|
||||||
|
"xWasRemovedDueToErrorY": "{} был удален из-за ошибки: {}",
|
||||||
|
"completeAppInstallation": "Завершение установки приложения",
|
||||||
|
"obtainiumMustBeOpenToInstallApps": "Для установки приложений Obtainium должен быть открыт",
|
||||||
|
"completeAppInstallationNotifDescription": "Просит пользователя вернуться в Obtainium, чтобы завершить установку приложения",
|
||||||
|
"checkingForUpdates": "Проверка обновлений",
|
||||||
|
"checkingForUpdatesNotifDescription": "Временное уведомление, которое появляется при проверке обновлений",
|
||||||
|
"pleaseAllowInstallPerm": "Пожалуйста, разрешите Obtainium устанавливать приложения",
|
||||||
|
"trackOnly": "Только отслеживать",
|
||||||
|
"errorWithHttpStatusCode": "Ошибка {}",
|
||||||
|
"versionCorrectionDisabled": "Коррекция версий отключена (плагин, кажется, не работает)",
|
||||||
|
"unknown": "Неизвестно",
|
||||||
|
"none": "Отсутствует",
|
||||||
|
"never": "Никогда",
|
||||||
|
"latestVersionX": "Последняя версия: {}",
|
||||||
|
"installedVersionX": "Установленная версия: {}",
|
||||||
|
"lastUpdateCheckX": "Последняя проверка обновлений: {}",
|
||||||
|
"remove": "Удалить",
|
||||||
|
"yesMarkUpdated": "Да, отметить как обновленное",
|
||||||
|
"fdroid": "Официальный F-Droid",
|
||||||
|
"appIdOrName": "ID или название приложения",
|
||||||
|
"appId": "ID приложения",
|
||||||
|
"appWithIdOrNameNotFound": "Приложение с таким ID или названием не было найдено",
|
||||||
|
"reposHaveMultipleApps": "В хранилище может быть несколько приложений",
|
||||||
|
"fdroidThirdPartyRepo": "Хранилище F-Droid сторонних разработчиков",
|
||||||
|
"steam": "Steam",
|
||||||
|
"steamMobile": "Steam Mobile",
|
||||||
|
"steamChat": "Steam Chat",
|
||||||
|
"install": "Установить",
|
||||||
|
"markInstalled": "Пометить как установленное",
|
||||||
|
"update": "Обновить",
|
||||||
|
"markUpdated": "Отметить обновленным",
|
||||||
|
"additionalOptions": "Дополнительные опции",
|
||||||
|
"disableVersionDetection": "Отключить обнаружение версии",
|
||||||
|
"noVersionDetectionExplanation": "Эта опция должна использоваться только для приложений, где обнаружение версии не работает корректно.",
|
||||||
|
"downloadingX": "Загрузка {}",
|
||||||
|
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
|
||||||
|
"noAPKFound": "APK не найден",
|
||||||
|
"noVersionDetection": "Версий не обнаружено",
|
||||||
|
"categorize": "Категоризировать",
|
||||||
|
"categories": "Категории",
|
||||||
|
"category": "Категория",
|
||||||
|
"noCategory": "Без категории",
|
||||||
|
"noCategories": "Без категорий",
|
||||||
|
"deleteCategoriesQuestion": "Удалить категории?",
|
||||||
|
"categoryDeleteWarning": "Все приложения в удаленных категориях будут помечены как без категории.",
|
||||||
|
"addCategory": "Добавить категорию",
|
||||||
|
"label": "Метка",
|
||||||
|
"language": "Язык",
|
||||||
|
"copiedToClipboard": "Скопировано в буфер обмена",
|
||||||
|
"storagePermissionDenied": "Отказано в доступе к хранилищу",
|
||||||
|
"selectedCategorizeWarning": "Это заменит все текущие настройки категорий для выбранных приложений.",
|
||||||
|
"filterAPKsByRegEx": "Фильтровать APK-файлы с помощью регулярного выражения",
|
||||||
|
"removeFromObtainium": "Удалить из Obtainium",
|
||||||
|
"uninstallFromDevice": "Удалить с устройства",
|
||||||
|
"onlyWorksWithNonVersionDetectApps": "Работает только для приложений с отключенным определением версии.",
|
||||||
|
"releaseDateAsVersion": "Использовать дату выпуска в качестве версии",
|
||||||
|
"releaseDateAsVersionExplanation": "Этот параметр следует использовать только для приложений, в которых определение версии не работает правильно, но имеется дата выпуска.",
|
||||||
|
"changes": "Изменения",
|
||||||
|
"releaseDate": "Дата выпуска",
|
||||||
|
"importFromURLsInFile": "Импортировать из URL-адресов в файл (например, OPML)",
|
||||||
|
"versionDetection": "Определение версии",
|
||||||
|
"standardVersionDetection": "Стандартное определение версии",
|
||||||
|
"groupByCategory": "Группировать по категориям",
|
||||||
|
"autoApkFilterByArch": "Попытка фильтрации APK-файлов по архитектуре процессора, если это возможно",
|
||||||
|
"overrideSource": "Переопределить источник",
|
||||||
|
"dontShowAgain": "Не показывать снова",
|
||||||
|
"dontShowTrackOnlyWarnings": "Не показывать предупреждения о только отслеживаемых приложениях",
|
||||||
|
"dontShowAPKOriginWarnings": "Не показывать предупреждения об источнике APK-файлов",
|
||||||
|
"moveNonInstalledAppsToBottom": "Переместить неустановленные приложения вниз списка",
|
||||||
|
"gitlabPATLabel": "Персональный токен доступа GitLab (Включает поиск)",
|
||||||
|
"about": "О приложении",
|
||||||
|
"requiresCredentialsInSettings": "Для этого требуются дополнительные учетные данные (в настройках)",
|
||||||
|
"checkOnStart": "Проверить один раз при запуске",
|
||||||
|
"tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
|
||||||
|
"removeAppQuestion": {
|
||||||
|
"one": "Удалить приложение?",
|
||||||
|
"other": "Удалить приложения?"
|
||||||
|
},
|
||||||
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
|
"one": "Слишком много запросов (ограничение скорости) - попробуйте снова через {} минуту",
|
||||||
|
"other": "Слишком много запросов (ограничение скорости) - попробуйте снова через {} минуты"
|
||||||
|
},
|
||||||
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
|
"one": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуту",
|
||||||
|
"other": "При проверке обновлений в фоновом режиме возникла ошибка {}, повторная проверка будет запланирована через {} минуты"
|
||||||
|
},
|
||||||
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
|
"one": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновление - Пользователю будет отправлено уведомление, если это необходимо",
|
||||||
|
"other": "В ходе проверки обновления в фоновом режиме было обнаружено {} обновлений - Пользователю будет отправлено уведомление, если это необходимо"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"one": "{} Приложение",
|
||||||
|
"other": "{} Приложений"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"one": "{} Ссылка",
|
||||||
|
"other": "{} Ссылки"
|
||||||
|
},
|
||||||
|
"minute": {
|
||||||
|
"one": "{} Минута",
|
||||||
|
"other": "{} Минуты"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"one": "{} Час",
|
||||||
|
"other": "{} Часов"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"one": "{} День",
|
||||||
|
"other": "{} Дней"
|
||||||
|
},
|
||||||
|
"clearedNLogsBeforeXAfterY": {
|
||||||
|
"one": "Очищен {n} журнал (до = {before}, после = {after})",
|
||||||
|
"other": "Очищено {n} журналов (до = {before}, после = {after})"
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesAvailable": {
|
||||||
|
"one": "У {} и еще 1 приложения есть обновления.",
|
||||||
|
"other": "{} and {} more apps have updates."
|
||||||
|
},
|
||||||
|
"xAndNMoreUpdatesInstalled": {
|
||||||
|
"one": "{} and 1 more app were updated.",
|
||||||
|
"other": "У {} и еще {} приложений есть обновления."
|
||||||
|
}
|
||||||
|
}
|
@ -1,106 +1,104 @@
|
|||||||
{
|
{
|
||||||
"invalidURLForSource": "不是一个有效的 {} URL",
|
"invalidURLForSource": "无效的 {} URL",
|
||||||
"noReleaseFound": "找不到合适的更新",
|
"noReleaseFound": "找不到合适的发行版",
|
||||||
"noVersionFound": "无法确定更新版本",
|
"noVersionFound": "无法确定发行版本号",
|
||||||
"urlMatchesNoSource": "URL 与已知来源不符",
|
"urlMatchesNoSource": "URL 与已知的来源不符",
|
||||||
"cantInstallOlderVersion": "无法安装旧版应用程序",
|
"cantInstallOlderVersion": "无法安装旧版本的应用",
|
||||||
"appIdMismatch": "下载的软件包名与现有的应用程序包名不一致",
|
"appIdMismatch": "所下载 APK 的应用 ID 与现有应用不一致",
|
||||||
"functionNotImplemented": "该类没有实现此功能",
|
"functionNotImplemented": "该类未实现此功能",
|
||||||
"placeholder": "占位符",
|
"placeholder": "占位符",
|
||||||
"someErrors": "出现了一些错误",
|
"someErrors": "出现了一些错误",
|
||||||
"unexpectedError": "意外错误",
|
"unexpectedError": "意外错误",
|
||||||
"ok": "好的",
|
"ok": "好的",
|
||||||
"and": "和",
|
"and": "和",
|
||||||
"startedBgUpdateTask": "开始后台检查更新任务",
|
"startedBgUpdateTask": "后台更新检查任务已启动",
|
||||||
"bgUpdateIgnoreAfterIs": "下次后台更新检查 {}",
|
"bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}",
|
||||||
"startedActualBGUpdateCheck": "后台检查更新已开始",
|
"startedActualBGUpdateCheck": "开始后台更新检查",
|
||||||
"bgUpdateTaskFinished": "后台检查更新已完成",
|
"bgUpdateTaskFinished": "后台更新检查任务已完成",
|
||||||
"firstRun": "这是你第一次运行 Obtainium",
|
"firstRun": "这是 Obtainium 首次启动",
|
||||||
"settingUpdateCheckIntervalTo": "设置检查更新间隔为 {}",
|
"settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}",
|
||||||
"githubPATLabel": "GitHub 个人访问令牌 (提高 API 限制)",
|
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||||
"githubPATHint": "个人访问令牌必须为: username:token 形式",
|
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username:token",
|
||||||
"githubPATLinkText": "关于 GitHub 个人访问令牌",
|
"includePrereleases": "包含预发行版",
|
||||||
"includePrereleases": "包含预发布版",
|
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||||
"fallbackToOlderReleases": "回退到旧版",
|
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
||||||
"filterReleaseTitlesByRegEx": "使用正则以过滤发布标题",
|
"invalidRegEx": "无效的正则表达式",
|
||||||
"invalidRegEx": "表达式无效",
|
|
||||||
"noDescription": "无描述",
|
"noDescription": "无描述",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"continue": "继续",
|
"continue": "继续",
|
||||||
"requiredInBrackets": "(必须)",
|
"requiredInBrackets": "(必填)",
|
||||||
"dropdownNoOptsError": "错误:下拉菜单必须至少有一个选项",
|
"dropdownNoOptsError": "错误:下拉菜单必须包含至少一个选项",
|
||||||
"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": "此应用已经添加",
|
||||||
"alreadyUpToDateQuestion": "应用已是最新?",
|
"alreadyUpToDateQuestion": "应用是否已经为最新版本?",
|
||||||
"addApp": "添加应用",
|
"addApp": "添加应用",
|
||||||
"appSourceURL": "应用来源 URL",
|
"appSourceURL": "来源 URL",
|
||||||
"error": "错误",
|
"error": "错误",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"searchSomeSourcesLabel": "搜索 (仅部分来源)",
|
"searchSomeSourcesLabel": "搜索(仅部分来源)",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"additionalOptsFor": "{} 的更多选项",
|
"additionalOptsFor": "{} 的更多选项",
|
||||||
"supportedSourcesBelow": "受支持的来源:",
|
"supportedSourcesBelow": "支持的来源:",
|
||||||
"trackOnlyInBrackets": "(仅追踪)",
|
"trackOnlyInBrackets": "(仅追踪)",
|
||||||
"searchableInBrackets": "(可被搜索)",
|
"searchableInBrackets": "(可搜索)",
|
||||||
"appsString": "应用程序",
|
"appsString": "应用列表",
|
||||||
"noApps": "无应用程序",
|
"noApps": "无应用",
|
||||||
"noAppsForFilter": "没有应用可被过滤",
|
"noAppsForFilter": "没有符合条件的应用",
|
||||||
"byX": "来自 {}",
|
"byX": "作者:{}",
|
||||||
"percentProgress": "进度: {}%",
|
"percentProgress": "进度:{}%",
|
||||||
"pleaseWait": "请等待...",
|
"pleaseWait": "请稍候",
|
||||||
"updateAvailable": "更新可用",
|
"updateAvailable": "更新可用",
|
||||||
"estimateInBracketsShort": "(预计.)",
|
"estimateInBracketsShort": "(推测)",
|
||||||
"notInstalled": "未安装",
|
"notInstalled": "未安装",
|
||||||
"estimateInBrackets": "(预计)",
|
"estimateInBrackets": "(推测)",
|
||||||
"selectAll": "全选",
|
"selectAll": "全选",
|
||||||
"deselectN": "取消选择 {}",
|
"deselectN": "取消选择 {}",
|
||||||
"xWillBeRemovedButRemainInstalled": "{} 将被从 Obtainium 中删除,但仍安装在设备上。",
|
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||||
"removeSelectedAppsQuestion": "删除已选择的应用程序吗?",
|
"removeSelectedAppsQuestion": "是否删除选中的应用?",
|
||||||
"removeSelectedApps": "删除已选择的应用程序",
|
"removeSelectedApps": "删除选中的应用",
|
||||||
"updateX": "更新 {}",
|
"updateX": "更新 {}",
|
||||||
"installX": "安装 {}",
|
"installX": "安装 {}",
|
||||||
"markXTrackOnlyAsUpdated": "将仅追踪编辑为已更新",
|
"markXTrackOnlyAsUpdated": "将 {}\n(仅追踪)\n标记为已更新",
|
||||||
"changeX": "更改 {}",
|
"changeX": "更改 {}",
|
||||||
"installUpdateApps": "安装/更新应用程序",
|
"installUpdateApps": "安装/更新应用",
|
||||||
"installUpdateSelectedApps": "安装/更新已选择的应用程序",
|
"installUpdateSelectedApps": "安装/更新选中的应用",
|
||||||
"onlyAppliesToInstalledAndOutdatedApps": "'只适用于已安装但已过时的应用程序",
|
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
||||||
"markXSelectedAppsAsUpdated": "将已选择的 {} 个应用程序标记为已更新?",
|
"no": "否",
|
||||||
"no": "不要",
|
"yes": "是",
|
||||||
"yes": "好的",
|
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
||||||
"markSelectedAppsUpdated": "标记已选择的应用程序为已更新",
|
|
||||||
"pinToTop": "置顶",
|
"pinToTop": "置顶",
|
||||||
"unpinFromTop": "取消置顶",
|
"unpinFromTop": "取消置顶",
|
||||||
"resetInstallStatusForSelectedAppsQuestion": "为已选择的应用程序重置安装状态吗?",
|
"resetInstallStatusForSelectedAppsQuestion": "是否重置选中应用的安装状态?",
|
||||||
"installStatusOfXWillBeResetExplanation": "当 Obtainium 中显示的应用程序版本由于更新失败或其他问题而不正确时,这将有助于重置任何选定应用程序的安装状态。",
|
"installStatusOfXWillBeResetExplanation": "选中应用的安装状态将会被重置。\n\n当更新安装失败或其他问题导致 Obtainium 中的应用版本显示错误时,可以尝试通过此方法解决。",
|
||||||
"shareSelectedAppURLs": "分享已选择的应用程序 URL",
|
"shareSelectedAppURLs": "分享选中应用的 URL",
|
||||||
"resetInstallStatus": "重置安装状态",
|
"resetInstallStatus": "重置安装状态",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
"removeOutdatedFilter": "删除过时的应用程序过滤器",
|
"removeOutdatedFilter": "删除失效的应用筛选",
|
||||||
"showOutdatedOnly": "只显示过时的应用程序",
|
"showOutdatedOnly": "只显示待更新应用",
|
||||||
"filter": "过滤器",
|
"filter": "筛选",
|
||||||
"filterActive": "过滤器 *",
|
"filterActive": "筛选 *",
|
||||||
"filterApps": "过滤应用",
|
"filterApps": "筛选应用",
|
||||||
"appName": "应用名称",
|
"appName": "应用名称",
|
||||||
"author": "作者",
|
"author": "作者",
|
||||||
"upToDateApps": "已更新的应用程序",
|
"upToDateApps": "无需更新的应用",
|
||||||
"nonInstalledApps": "未安装的应用程序",
|
"nonInstalledApps": "未安装的应用",
|
||||||
"importExport": "导入/导出",
|
"importExport": "导入/导出",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"exportedTo": "导出到 {}",
|
"exportedTo": "已导出至 {}",
|
||||||
"obtainiumExport": "Obtainium 导出",
|
"obtainiumExport": "Obtainium 导出",
|
||||||
"invalidInput": "无效输入",
|
"invalidInput": "无效的输入",
|
||||||
"importedX": "已导出到 {}",
|
"importedX": "已导入 {}",
|
||||||
"obtainiumImport": "Obtainium 导入",
|
"obtainiumImport": "Obtainium 导入",
|
||||||
"importFromURLList": "从 URL 列表导入",
|
"importFromURLList": "从 URL 列表导入",
|
||||||
"searchQuery": "搜索查询",
|
"searchQuery": "搜索查询",
|
||||||
@ -109,13 +107,13 @@
|
|||||||
"searchX": "搜索 {}",
|
"searchX": "搜索 {}",
|
||||||
"noResults": "无结果",
|
"noResults": "无结果",
|
||||||
"importX": "导入 {}",
|
"importX": "导入 {}",
|
||||||
"importedAppsIdDisclaimer": "导入的应用程序可能显示为未安装。要解决这个问题,请通过 Obtainium 重新安装它们。",
|
"importedAppsIdDisclaimer": "导入的应用可能错误地显示为“未安装”。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
|
||||||
"importErrors": "导入错误",
|
"importErrors": "导入错误",
|
||||||
"importedXOfYApps": "{} 中的 {} 个应用已导入",
|
"importedXOfYApps": "已导入 {} 中的 {} 个应用。",
|
||||||
"followingURLsHadErrors": "以下 URL 有错误:",
|
"followingURLsHadErrors": "下列 URL 存在错误:",
|
||||||
"okay": "好的",
|
"okay": "好的",
|
||||||
"selectURL": "已选择的 URL",
|
"selectURL": "选择 URL",
|
||||||
"selectURLs": "已选择的 URL",
|
"selectURLs": "选择 URL",
|
||||||
"pick": "选择",
|
"pick": "选择",
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
@ -123,67 +121,69 @@
|
|||||||
"followSystem": "跟随系统",
|
"followSystem": "跟随系统",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"appSortBy": "排列方式",
|
"useBlackTheme": "使用纯黑深色主题",
|
||||||
"authorName": "作者 / 名字",
|
"appSortBy": "排序依据",
|
||||||
"nameAuthor": "名字 / 作者",
|
"authorName": "作者 / 应用名称",
|
||||||
"asAdded": "添加顺序",
|
"nameAuthor": "应用名称 / 作者",
|
||||||
"appSortOrder": "排列顺序",
|
"asAdded": "添加次序",
|
||||||
|
"appSortOrder": "顺序",
|
||||||
"ascending": "升序",
|
"ascending": "升序",
|
||||||
"descending": "降序",
|
"descending": "降序",
|
||||||
"bgUpdateCheckInterval": "后台更新检查间隔",
|
"bgUpdateCheckInterval": "后台更新检查间隔",
|
||||||
"neverManualOnly": "手动",
|
"neverManualOnly": "手动",
|
||||||
"appearance": "外观",
|
"appearance": "外观",
|
||||||
"showWebInAppView": "在应用来源页显示网页",
|
"showWebInAppView": "在应用详情页显示来源网页",
|
||||||
"pinUpdates": "需更新的应用置顶",
|
"pinUpdates": "将待更新应用置顶",
|
||||||
"updates": "检查间隔",
|
"updates": "更新",
|
||||||
"sourceSpecific": "Github 访问令牌",
|
"sourceSpecific": "来源",
|
||||||
"appSource": "源代码",
|
"appSource": "源代码",
|
||||||
"noLogs": "无日志",
|
"noLogs": "无日志",
|
||||||
"appLogs": "应用日志",
|
"appLogs": "日志",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"appNotFound": "未找到应用",
|
"appNotFound": "未找到应用",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
"pickAnAPK": "选择一个安装包",
|
"pickAnAPK": "选择一个 APK 文件",
|
||||||
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
||||||
"deviceSupportsXArch": "你的设备支持 {} 架构",
|
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
||||||
"deviceSupportsFollowingArchs": "你的设备支持以下架构:",
|
"deviceSupportsFollowingArchs": "您的设备支持下列架构:",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"sourceIsXButPackageFromYPrompt": "此应用来源是 '{}' 但更新包来自 '{}'。 继续吗?",
|
"sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?",
|
||||||
"updatesAvailable": "更新可用",
|
"updatesAvailable": "更新可用",
|
||||||
"updatesAvailableNotifDescription": "通知 Obtainium 所跟踪应用程序的更新",
|
"updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发出通知",
|
||||||
"noNewUpdates": "你的应用已是最新。",
|
"noNewUpdates": "全部应用已是最新。",
|
||||||
"xHasAnUpdate": "{} 有更新啦",
|
"xHasAnUpdate": "{} 可以更新了。",
|
||||||
"appsUpdated": "应用已更新",
|
"appsUpdated": "应用已更新",
|
||||||
"appsUpdatedNotifDescription": "通知在后台安装应用程序的更新",
|
"appsUpdatedNotifDescription": "当应用在后台安装更新时发出通知",
|
||||||
"xWasUpdatedToY": "{} 已更新到 {}.",
|
"xWasUpdatedToY": "{} 已更新至 {}。",
|
||||||
"errorCheckingUpdates": "检查更新出错",
|
"errorCheckingUpdates": "检查更新出错",
|
||||||
"errorCheckingUpdatesNotifDescription": "当后台更新检查失败时显示的通知",
|
"errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知",
|
||||||
"appsRemoved": "应用已删除",
|
"appsRemoved": "应用已删除",
|
||||||
"appsRemovedNotifDescription": "通知由于加载应用程序时出错而被删除",
|
"appsRemovedNotifDescription": "当应用因加载出错而被删除时发出通知",
|
||||||
"xWasRemovedDueToErrorY": "{} 已因以下错误被删除: {}",
|
"xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}",
|
||||||
"completeAppInstallation": "完成应用安装",
|
"completeAppInstallation": "完成应用安装",
|
||||||
"obtainiumMustBeOpenToInstallApps": "Obtainium 需要被启动以安装更新",
|
"obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用",
|
||||||
"completeAppInstallationNotifDescription": "需要返回 Obtainium,以完成应用程序的安装。",
|
"completeAppInstallationNotifDescription": "提示返回 Obtainium 以完成应用的安装",
|
||||||
"checkingForUpdates": "检查更新中",
|
"checkingForUpdates": "正在检查更新",
|
||||||
"checkingForUpdatesNotifDescription": "检查更新时出现的瞬时通知",
|
"checkingForUpdatesNotifDescription": "检查更新时短暂显示的通知",
|
||||||
"pleaseAllowInstallPerm": "请允许 Obtainium 安装应用程序",
|
"pleaseAllowInstallPerm": "请授予 Obtainium 安装应用的权限",
|
||||||
"trackOnly": "仅追踪",
|
"trackOnly": "仅追踪",
|
||||||
"errorWithHttpStatusCode": "错误 {}",
|
"errorWithHttpStatusCode": "错误 {}",
|
||||||
"versionCorrectionDisabled": "禁用版本更正(插件似乎未起作用)",
|
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"none": "无",
|
"none": "无",
|
||||||
"never": "从不",
|
"never": "从未",
|
||||||
"latestVersionX": "最新: {}",
|
"latestVersionX": "最新版本:{}",
|
||||||
"installedVersionX": "已安装: {}",
|
"installedVersionX": "当前版本:{}",
|
||||||
"lastUpdateCheckX": "最后检查: {}",
|
"lastUpdateCheckX": "上次更新检查:{}",
|
||||||
"remove": "删除",
|
"remove": "删除",
|
||||||
"yesMarkUpdated": "'是的,标为已更新",
|
"yesMarkUpdated": "是,标记为已更新",
|
||||||
"fdroid": "F-Droid",
|
"fdroid": "F-Droid 官方存储库",
|
||||||
"appIdOrName": "应用 ID 或名称",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
"appWithIdOrNameNotFound": "没有发现具有此 ID 或名称的应用",
|
"appId": "App ID",
|
||||||
"reposHaveMultipleApps": "来源可能包含多个应用",
|
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||||
"fdroidThirdPartyRepo": "F-Droid 第三方源",
|
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||||
|
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
|
||||||
"steam": "Steam",
|
"steam": "Steam",
|
||||||
"steamMobile": "Steam Mobile",
|
"steamMobile": "Steam Mobile",
|
||||||
"steamChat": "Steam Chat",
|
"steamChat": "Steam Chat",
|
||||||
@ -192,50 +192,63 @@
|
|||||||
"update": "更新",
|
"update": "更新",
|
||||||
"markUpdated": "标记为已更新",
|
"markUpdated": "标记为已更新",
|
||||||
"additionalOptions": "附加选项",
|
"additionalOptions": "附加选项",
|
||||||
"disableVersionDetection": "关闭版本检测",
|
"disableVersionDetection": "禁用版本检测",
|
||||||
"noVersionDetectionExplanation": "此选项应只用于版本检测不能工作的应用程序",
|
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||||
"downloadingX": "下载中 {}",
|
"downloadingX": "正在下载{}",
|
||||||
"downloadNotifDescription": "通知用户下载进度",
|
"downloadNotifDescription": "提示应用的下载进度",
|
||||||
"noAPKFound": "未找到安装包",
|
"noAPKFound": "未找到 APK 文件",
|
||||||
"noVersionDetection": "无版本检测",
|
"noVersionDetection": "禁用版本检测",
|
||||||
"categorize": "归档",
|
"categorize": "分类",
|
||||||
"categories": "归档",
|
"categories": "类别",
|
||||||
"category": "类别",
|
"category": "类别",
|
||||||
"noCategory": "无类别",
|
"noCategory": "无类别",
|
||||||
"noCategories": "无类别",
|
"noCategories": "无类别",
|
||||||
"deleteCategoriesQuestion": "删除所有类别?",
|
"deleteCategoriesQuestion": "是否删除选中的类别?",
|
||||||
"categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
|
"categoryDeleteWarning": "被删除类别下的应用将恢复为未分类状态。",
|
||||||
"addCategory": "添加类别",
|
"addCategory": "添加类别",
|
||||||
"label": "标签",
|
"label": "标签",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"storagePermissionDenied": "存储权限已被拒绝",
|
"copiedToClipboard": "已复制至剪贴板",
|
||||||
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
"storagePermissionDenied": "已拒绝授予存储权限",
|
||||||
"filterAPKsByRegEx": "Filter APKs by Regular Expression",
|
"selectedCategorizeWarning": "这将覆盖选中应用当前的类别设置。",
|
||||||
"removeFromObtainium": "Remove from Obtainium",
|
"filterAPKsByRegEx": "使用正则表达式筛选 APK 文件",
|
||||||
"uninstallFromDevice": "Uninstall from Device",
|
"removeFromObtainium": "从 Obtainium 中删除",
|
||||||
"releaseDateAsVersion": "Use Release Date as Version",
|
"uninstallFromDevice": "从设备中卸载",
|
||||||
"releaseDateAsVersionExplanation": "This option should only be used for Apps where version detection does not work correctly, but a release date is available.",
|
"onlyWorksWithNonVersionDetectApps": "仅适用于禁用版本检测的应用。",
|
||||||
"changes": "Changes",
|
"releaseDateAsVersion": "将发行日期作为版本号",
|
||||||
"releaseDate": "Release Date",
|
"releaseDateAsVersionExplanation": "此选项应该仅用于无法进行版本检测但能够获取发行日期的应用。",
|
||||||
"importFromURLsInFile": "Import from URLs in File (like OPML)",
|
"changes": "更新日志",
|
||||||
"versionDetection": "Version Detection",
|
"releaseDate": "发行日期",
|
||||||
"standardVersionDetection": "Standard version detection",
|
"importFromURLsInFile": "从文件中的 URL 导入(如 OPML)",
|
||||||
"groupByCategory": "Group by Category",
|
"versionDetection": "版本检测",
|
||||||
|
"standardVersionDetection": "常规版本检测",
|
||||||
|
"groupByCategory": "按类别分组显示",
|
||||||
|
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
|
||||||
|
"overrideSource": "覆盖来源",
|
||||||
|
"dontShowAgain": "不再显示",
|
||||||
|
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
|
||||||
|
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
|
||||||
|
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||||
|
"gitlabPATLabel": "GitLab 个人访问令牌(用于搜索应用)",
|
||||||
|
"about": "相关文档",
|
||||||
|
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
|
||||||
|
"checkOnStart": "启动时进行一次检查",
|
||||||
|
"tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "删除应用?"
|
"other": "是否删除应用?"
|
||||||
},
|
},
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "API 请求过于频繁(速率限制)- 在 {} 分钟后重试"
|
||||||
},
|
},
|
||||||
"bgUpdateGotErrorRetryInMinutes": {
|
"bgUpdateGotErrorRetryInMinutes": {
|
||||||
"one": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试",
|
"one": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试",
|
||||||
"other": "后台更新检查遇到了 {} 问题, 将在 {} 分钟后重试"
|
"other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试"
|
||||||
},
|
},
|
||||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||||
"one": "后台更新检查找到了 {} 个更新 - 将通知用户",
|
"one": "后台检查发现 {} 个应用更新 - 如有需要将发出通知",
|
||||||
"other": "后台更新检查找到了 {} 个更新 - 将通知用户"
|
"other": "后台检查发现 {} 个应用更新 - 如有需要将发出通知"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"one": "{} 个应用",
|
"one": "{} 个应用",
|
||||||
@ -258,15 +271,15 @@
|
|||||||
"other": "{} 天"
|
"other": "{} 天"
|
||||||
},
|
},
|
||||||
"clearedNLogsBeforeXAfterY": {
|
"clearedNLogsBeforeXAfterY": {
|
||||||
"one": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})",
|
"one": "清除了 {n} 个日志({before} 之前,{after} 之后)",
|
||||||
"other": "清除了 {n} 个日志 (清除前 = {before}, 清除后 = {after})"
|
"other": "清除了 {n} 个日志({before} 之前,{after} 之后)"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesAvailable": {
|
"xAndNMoreUpdatesAvailable": {
|
||||||
"one": "{} 和 {} 更多应用已被更新",
|
"one": "{} 和另外 1 个应用可以更新了。",
|
||||||
"other": "{} 和 {} 更多应用已被更新"
|
"other": "{} 和另外 {} 个应用可以更新了。"
|
||||||
},
|
},
|
||||||
"xAndNMoreUpdatesInstalled": {
|
"xAndNMoreUpdatesInstalled": {
|
||||||
"one": "{} 和 {} 更多应用已被安装",
|
"one": "{} 和另外 1 个应用已更新。",
|
||||||
"other": "{} 和 {} 更多应用已被安装"
|
"other": "{} 和另外 {} 个应用已更新。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
114
lib/app_sources/apkcombo.dart
Normal file
114
lib/app_sources/apkcombo.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class APKCombo extends AppSource {
|
||||||
|
APKCombo() {
|
||||||
|
host = 'apkcombo.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
|
||||||
|
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> get requestHeaders => {
|
||||||
|
"User-Agent": "curl/8.0.1",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Host": "$host"
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
|
||||||
|
var res = await sourceRequest('$standardUrl/download/apk');
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var html = parse(res.body);
|
||||||
|
return html
|
||||||
|
.querySelectorAll('#variants-tab > div > ul > li')
|
||||||
|
.map((e) {
|
||||||
|
String? arch = e
|
||||||
|
.querySelector('code')
|
||||||
|
?.text
|
||||||
|
.trim()
|
||||||
|
.replaceAll(',', '')
|
||||||
|
.replaceAll(':', '-')
|
||||||
|
.replaceAll(' ', '-');
|
||||||
|
return e.querySelectorAll('a').map((e) {
|
||||||
|
String? url = e.attributes['href'];
|
||||||
|
if (url != null &&
|
||||||
|
!Uri.parse(url).path.toLowerCase().endsWith('.apk')) {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
String verCode =
|
||||||
|
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
|
||||||
|
return MapEntry<String, String>(
|
||||||
|
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
|
||||||
|
}).toList();
|
||||||
|
})
|
||||||
|
.reduce((value, element) => [...value, ...element])
|
||||||
|
.where((element) => element.value.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> apkUrlPrefetchModifier(
|
||||||
|
String apkUrl, String standardUrl) async {
|
||||||
|
var freshURLs = await getApkUrls(standardUrl);
|
||||||
|
var path2Match = Uri.parse(apkUrl).path;
|
||||||
|
for (var url in freshURLs) {
|
||||||
|
if (Uri.parse(url.value).path == path2Match) {
|
||||||
|
return url.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
|
var preres = await sourceRequest(standardUrl);
|
||||||
|
if (preres.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(preres);
|
||||||
|
}
|
||||||
|
var res = parse(preres.body);
|
||||||
|
String? version = res.querySelector('div.version')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String appName = res.querySelector('div.app_name')?.text.trim() ?? appId;
|
||||||
|
String author = res.querySelector('div.author')?.text.trim() ?? appName;
|
||||||
|
List<String> infoArray = res
|
||||||
|
.querySelectorAll('div.information-table > .item > div.value')
|
||||||
|
.map((e) => e.text.trim())
|
||||||
|
.toList();
|
||||||
|
DateTime? releaseDate;
|
||||||
|
if (infoArray.length >= 2) {
|
||||||
|
try {
|
||||||
|
releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, await getApkUrls(standardUrl), AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate);
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,7 @@ class APKMirror extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -57,7 +57,7 @@ class APKMirror extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse('$standardUrl/feed'));
|
Response res = await sourceRequest('$standardUrl/feed');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var items = parse(res.body).querySelectorAll('item');
|
var items = parse(res.body).querySelectorAll('item');
|
||||||
dynamic targetRelease;
|
dynamic targetRelease;
|
||||||
|
82
lib/app_sources/apkpure.dart
Normal file
82
lib/app_sources/apkpure.dart
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class APKPure extends AppSource {
|
||||||
|
APKPure() {
|
||||||
|
host = 'apkpure.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
|
if (match != null) {
|
||||||
|
url = 'https://$host/${Uri.parse(url).path}';
|
||||||
|
}
|
||||||
|
RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+');
|
||||||
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
String appId = (await tryInferringAppId(standardUrl))!;
|
||||||
|
String host = Uri.parse(standardUrl).host;
|
||||||
|
var res = await sourceRequest('$standardUrl/download');
|
||||||
|
var resChangelog = await sourceRequest(standardUrl);
|
||||||
|
if (res.statusCode == 200 && resChangelog.statusCode == 200) {
|
||||||
|
var html = parse(res.body);
|
||||||
|
var htmlChangelog = parse(resChangelog.body);
|
||||||
|
String? version = html.querySelector('span.info-sdk span')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? dateString =
|
||||||
|
html.querySelector('span.info-other span.date')?.text.trim();
|
||||||
|
DateTime? releaseDate;
|
||||||
|
try {
|
||||||
|
releaseDate = dateString != null
|
||||||
|
? DateFormat('MMM dd, yyyy').parse(dateString)
|
||||||
|
: null;
|
||||||
|
releaseDate = dateString != null && releaseDate == null
|
||||||
|
? DateFormat('MMMM dd, yyyy').parse(dateString)
|
||||||
|
: null;
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
|
List<MapEntry<String, String>> apkUrls = [
|
||||||
|
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||||
|
];
|
||||||
|
String author = html
|
||||||
|
.querySelector('span.info-sdk')
|
||||||
|
?.text
|
||||||
|
.trim()
|
||||||
|
.substring(version.length + 4) ??
|
||||||
|
Uri.parse(standardUrl).pathSegments.reversed.last;
|
||||||
|
String appName =
|
||||||
|
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
||||||
|
String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml
|
||||||
|
.trim().replaceAll("<br>", " \n");
|
||||||
|
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
changeLog: changeLog);
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:obtainium/app_sources/github.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/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -35,8 +34,10 @@ class Codeberg extends AppSource {
|
|||||||
canSearch = true;
|
canSearch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gh = GitHub();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -54,78 +55,10 @@ class Codeberg extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
return await gh.getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||||
bool fallbackToOlderReleases =
|
(bool useTagUrl) async {
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
return 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
String? regexFilter =
|
}, null);
|
||||||
(additionalSettings['filterReleaseTitlesByRegEx'] as String?)
|
|
||||||
?.isNotEmpty ==
|
|
||||||
true
|
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
|
||||||
: null;
|
|
||||||
Response res = await get(Uri.parse(
|
|
||||||
'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
|
||||||
|
|
||||||
List<String> getReleaseAPKUrls(dynamic release) =>
|
|
||||||
(release['assets'] as List<dynamic>?)
|
|
||||||
?.map((e) {
|
|
||||||
return e['name'] != null && e['browser_download_url'] != null
|
|
||||||
? MapEntry(e['name'] as String,
|
|
||||||
e['browser_download_url'] as String)
|
|
||||||
: const MapEntry('', '');
|
|
||||||
})
|
|
||||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
|
||||||
.map((e) => e.value)
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
dynamic targetRelease;
|
|
||||||
|
|
||||||
for (int i = 0; i < releases.length; i++) {
|
|
||||||
if (!fallbackToOlderReleases && i > 0) break;
|
|
||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (releases[i]['draft'] == true) {
|
|
||||||
// Draft releases not supported
|
|
||||||
}
|
|
||||||
var nameToFilter = releases[i]['name'] as String?;
|
|
||||||
if (nameToFilter == null || nameToFilter.trim().isEmpty) {
|
|
||||||
// Some leave titles empty so tag is used
|
|
||||||
nameToFilter = releases[i]['tag_name'] as String;
|
|
||||||
}
|
|
||||||
if (regexFilter != null &&
|
|
||||||
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
|
||||||
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
targetRelease = releases[i];
|
|
||||||
targetRelease['apkUrls'] = apkUrls;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (targetRelease == null) {
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
|
||||||
String? version = targetRelease['tag_name'];
|
|
||||||
DateTime? releaseDate = targetRelease['published_at'] != null
|
|
||||||
? DateTime.parse(targetRelease['published_at'])
|
|
||||||
: null;
|
|
||||||
if (version == null) {
|
|
||||||
throw NoVersionError();
|
|
||||||
}
|
|
||||||
var changeLog = targetRelease['body'].toString();
|
|
||||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
|
||||||
getAppNames(standardUrl),
|
|
||||||
releaseDate: releaseDate,
|
|
||||||
changeLog: changeLog.isEmpty ? null : changeLog);
|
|
||||||
} else {
|
|
||||||
throw getObtainiumHttpError(res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
@ -135,21 +68,10 @@ class Codeberg extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>> search(String query) async {
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
Response res = await get(Uri.parse(
|
return gh.searchCommon(
|
||||||
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100'));
|
query,
|
||||||
if (res.statusCode == 200) {
|
'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100',
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
'data');
|
||||||
for (var e in (jsonDecode(res.body)['data'] as List<dynamic>)) {
|
|
||||||
urlsWithDescriptions.addAll({
|
|
||||||
e['html_url'] as String: e['description'] != null
|
|
||||||
? e['description'] as String
|
|
||||||
: tr('noDescription')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return urlsWithDescriptions;
|
|
||||||
} else {
|
|
||||||
throw getObtainiumHttpError(res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -9,15 +10,17 @@ class FDroid extends AppSource {
|
|||||||
FDroid() {
|
FDroid() {
|
||||||
host = 'f-droid.org';
|
host = 'f-droid.org';
|
||||||
name = tr('fdroid');
|
name = tr('fdroid');
|
||||||
|
canSearch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegExB =
|
RegExp standardUrlRegExB =
|
||||||
RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
|
RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
url = 'https://$host/packages/${Uri.parse(url).pathSegments.last}';
|
url =
|
||||||
|
'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
|
||||||
}
|
}
|
||||||
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
|
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
|
||||||
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
@ -28,8 +31,8 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
return Uri.parse(standardUrl).pathSegments.last;
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +51,7 @@ class FDroid extends AppSource {
|
|||||||
.where((element) => element['versionName'] == latestVersion)
|
.where((element) => element['versionName'] == latestVersion)
|
||||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, apkUrls,
|
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||||
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -60,10 +63,40 @@ class FDroid extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
|
String host = Uri.parse(standardUrl).host;
|
||||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),
|
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||||
'https://f-droid.org/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl);
|
standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
|
Response res = await sourceRequest(
|
||||||
|
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
|
String? url = e.attributes['href'];
|
||||||
|
if (url != null) {
|
||||||
|
try {
|
||||||
|
standardizeUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url != null) {
|
||||||
|
urlsWithDescriptions[url] = [
|
||||||
|
e.querySelector('.package-name')?.text.trim() ?? '',
|
||||||
|
e.querySelector('.package-summary')?.text.trim() ??
|
||||||
|
tr('noDescription')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return urlsWithDescriptions;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.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/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -19,17 +18,6 @@ class FDroidRepo extends AppSource {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String standardizeURL(String url) {
|
|
||||||
RegExp standardUrlRegExp =
|
|
||||||
RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)');
|
|
||||||
RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase());
|
|
||||||
if (match == null) {
|
|
||||||
throw InvalidURLError(name);
|
|
||||||
}
|
|
||||||
return url.substring(0, match.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
@ -39,7 +27,7 @@ class FDroidRepo extends AppSource {
|
|||||||
if (appIdOrName == null) {
|
if (appIdOrName == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var res = await get(Uri.parse('$standardUrl/index.xml'));
|
var res = await sourceRequest('$standardUrl/index.xml');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var body = parse(res.body);
|
var body = parse(res.body);
|
||||||
var foundApps = body.querySelectorAll('application').where((element) {
|
var foundApps = body.querySelectorAll('application').where((element) {
|
||||||
@ -80,7 +68,8 @@ class FDroidRepo extends AppSource {
|
|||||||
element.querySelector('apkname') != null)
|
element.querySelector('apkname') != null)
|
||||||
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
.map((e) => '$standardUrl/${e.querySelector('apkname')!.innerHtml}')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, apkUrls, AppNames(authorName, appName),
|
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||||
|
AppNames(authorName, appName),
|
||||||
releaseDate: releaseDate);
|
releaseDate: releaseDate);
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
|
@ -2,8 +2,11 @@ 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:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.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/providers/apps_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';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -11,6 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||||||
class GitHub extends AppSource {
|
class GitHub extends AppSource {
|
||||||
GitHub() {
|
GitHub() {
|
||||||
host = 'github.com';
|
host = 'github.com';
|
||||||
|
appIdInferIsOptional = true;
|
||||||
|
|
||||||
additionalSourceSpecificSettingFormItems = [
|
additionalSourceSpecificSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField('github-creds',
|
||||||
@ -34,7 +38,7 @@ class GitHub extends AppSource {
|
|||||||
hint: tr('githubPATFormat'),
|
hint: tr('githubPATFormat'),
|
||||||
belowWidgets: [
|
belowWidgets: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 4,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -43,10 +47,13 @@ class GitHub extends AppSource {
|
|||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('githubPATLinkText'),
|
tr('about'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.underline, fontSize: 12),
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
))
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -75,7 +82,56 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
|
const possibleBuildGradleLocations = [
|
||||||
|
'/app/build.gradle',
|
||||||
|
'android/app/build.gradle',
|
||||||
|
'src/app/build.gradle'
|
||||||
|
];
|
||||||
|
for (var path in possibleBuildGradleLocations) {
|
||||||
|
try {
|
||||||
|
var res = await sourceRequest(
|
||||||
|
'${await convertStandardUrlToAPIUrl(standardUrl)}/contents/$path');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
try {
|
||||||
|
var body = jsonDecode(res.body);
|
||||||
|
var trimmedLines = utf8
|
||||||
|
.decode(base64
|
||||||
|
.decode(body['content'].toString().split('\n').join('')))
|
||||||
|
.split('\n')
|
||||||
|
.map((e) => e.trim());
|
||||||
|
var appId = trimmedLines
|
||||||
|
.where((l) =>
|
||||||
|
l.startsWith('applicationId "') ||
|
||||||
|
l.startsWith('applicationId \''))
|
||||||
|
.first;
|
||||||
|
appId = appId
|
||||||
|
.split(appId.startsWith('applicationId "') ? '"' : '\'')[1];
|
||||||
|
if (appId.startsWith('\${') && appId.endsWith('}')) {
|
||||||
|
appId = trimmedLines
|
||||||
|
.where((l) => l.startsWith(
|
||||||
|
'def ${appId.substring(2, appId.length - 1)}'))
|
||||||
|
.first;
|
||||||
|
appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
|
||||||
|
}
|
||||||
|
if (appId.isNotEmpty) {
|
||||||
|
return appId;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
LogsProvider().add(
|
||||||
|
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore - ID will be extracted from the APK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -92,15 +148,19 @@ class GitHub extends AppSource {
|
|||||||
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getAPIHost() async =>
|
||||||
|
'https://${await getCredentialPrefixIfAny()}api.$host';
|
||||||
|
|
||||||
|
Future<String> convertStandardUrlToAPIUrl(String standardUrl) async =>
|
||||||
|
'${await getAPIHost()}/repos${standardUrl.substring('https://$host'.length)}';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/releases';
|
'$standardUrl/releases';
|
||||||
|
|
||||||
@override
|
Future<APKDetails> getLatestAPKDetailsCommon(String requestUrl,
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
String standardUrl, Map<String, dynamic> additionalSettings,
|
||||||
String standardUrl,
|
{Function(Response)? onHttpErrorCode}) async {
|
||||||
Map<String, dynamic> additionalSettings,
|
|
||||||
) async {
|
|
||||||
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
bool includePrereleases = additionalSettings['includePrereleases'] == true;
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
@ -110,27 +170,63 @@ class GitHub extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await sourceRequest(requestUrl);
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100'));
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
List<String> getReleaseAPKUrls(dynamic release) =>
|
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||||
(release['assets'] as List<dynamic>?)
|
(release['assets'] as List<dynamic>?)
|
||||||
?.map((e) {
|
?.map((e) {
|
||||||
return e['browser_download_url'] != null
|
return e['name'] != null && e['browser_download_url'] != null
|
||||||
? e['browser_download_url'] as String
|
? MapEntry(e['name'] as String,
|
||||||
: '';
|
e['browser_download_url'] as String)
|
||||||
|
: const MapEntry('', '');
|
||||||
})
|
})
|
||||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
DateTime? getReleaseDateFromRelease(dynamic rel) =>
|
||||||
|
rel?['published_at'] != null
|
||||||
|
? DateTime.parse(rel['published_at'])
|
||||||
|
: null;
|
||||||
|
releases.sort((a, b) {
|
||||||
|
// See #478 and #534
|
||||||
|
if (a == b) {
|
||||||
|
return 0;
|
||||||
|
} else if (a == null) {
|
||||||
|
return -1;
|
||||||
|
} else if (b == null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
var nameA = a['tag_name'] ?? a['name'];
|
||||||
|
var nameB = b['tag_name'] ?? b['name'];
|
||||||
|
var stdFormats = findStandardFormatsForVersion(nameA, true)
|
||||||
|
.intersection(findStandardFormatsForVersion(nameB, true));
|
||||||
|
if (stdFormats.isNotEmpty) {
|
||||||
|
var reg = RegExp(stdFormats.first);
|
||||||
|
var matchA = reg.firstMatch(nameA);
|
||||||
|
var matchB = reg.firstMatch(nameB);
|
||||||
|
return compareAlphaNumeric(
|
||||||
|
(nameA as String).substring(matchA!.start, matchA.end),
|
||||||
|
(nameB as String).substring(matchB!.start, matchB.end));
|
||||||
|
} else {
|
||||||
|
return getReleaseDateFromRelease(a)!
|
||||||
|
.compareTo(getReleaseDateFromRelease(b)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
releases = releases.reversed.toList();
|
||||||
dynamic targetRelease;
|
dynamic targetRelease;
|
||||||
|
var prerrelsSkipped = 0;
|
||||||
for (int i = 0; i < releases.length; i++) {
|
for (int i = 0; i < releases.length; i++) {
|
||||||
if (!fallbackToOlderReleases && i > 0) break;
|
if (!fallbackToOlderReleases && i > prerrelsSkipped) break;
|
||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
|
prerrelsSkipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (releases[i]['draft'] == true) {
|
||||||
|
// Draft releases not supported
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var nameToFilter = releases[i]['name'] as String?;
|
var nameToFilter = releases[i]['name'] as String?;
|
||||||
@ -153,50 +249,101 @@ class GitHub extends AppSource {
|
|||||||
if (targetRelease == null) {
|
if (targetRelease == null) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String? version = targetRelease['tag_name'];
|
String? version = targetRelease['tag_name'] ?? targetRelease['name'];
|
||||||
DateTime? releaseDate = targetRelease['published_at'] != null
|
DateTime? releaseDate = getReleaseDateFromRelease(targetRelease);
|
||||||
? DateTime.parse(targetRelease['published_at'])
|
|
||||||
: null;
|
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
var changeLog = targetRelease['body'].toString();
|
var changeLog = targetRelease['body'].toString();
|
||||||
return APKDetails(version, targetRelease['apkUrls'] as List<String>,
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||||
getAppNames(standardUrl),
|
getAppNames(standardUrl),
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate,
|
||||||
changeLog: changeLog.isEmpty ? null : changeLog);
|
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||||
} else {
|
} else {
|
||||||
rateLimitErrorCheck(res);
|
if (onHttpErrorCode != null) {
|
||||||
|
onHttpErrorCode(res);
|
||||||
|
}
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLatestAPKDetailsCommon2(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
Future<String> Function(bool) reqUrlGenerator,
|
||||||
|
dynamic Function(Response)? onHttpErrorCode) async {
|
||||||
|
try {
|
||||||
|
return await getLatestAPKDetailsCommon(
|
||||||
|
await reqUrlGenerator(false), standardUrl, additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode);
|
||||||
|
} catch (err) {
|
||||||
|
if (err is NoReleasesError && additionalSettings['trackOnly'] == true) {
|
||||||
|
return await getLatestAPKDetailsCommon(
|
||||||
|
await reqUrlGenerator(true), standardUrl, additionalSettings,
|
||||||
|
onHttpErrorCode: onHttpErrorCode);
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||||
|
(bool useTagUrl) async {
|
||||||
|
return '${await convertStandardUrlToAPIUrl(standardUrl)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||||
|
}, (Response res) {
|
||||||
|
rateLimitErrorCheck(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
|
||||||
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
|
||||||
return AppNames(names[0], names[1]);
|
return AppNames(names[0], names[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<Map<String, List<String>>> searchCommon(
|
||||||
Future<Map<String, String>> search(String query) async {
|
String query, String requestUrl, String rootProp,
|
||||||
Response res = await get(Uri.parse(
|
{Function(Response)? onHttpErrorCode}) async {
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100'));
|
Response res = await sourceRequest(requestUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
||||||
urlsWithDescriptions.addAll({
|
urlsWithDescriptions.addAll({
|
||||||
e['html_url'] as String: e['description'] != null
|
e['html_url'] as String: [
|
||||||
? e['description'] as String
|
e['full_name'] as String,
|
||||||
: tr('noDescription')
|
((e['archived'] == true ? '[ARCHIVED] ' : '') +
|
||||||
|
(e['description'] != null
|
||||||
|
? e['description'] as String
|
||||||
|
: tr('noDescription')))
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
} else {
|
} else {
|
||||||
rateLimitErrorCheck(res);
|
if (onHttpErrorCode != null) {
|
||||||
|
onHttpErrorCode(res);
|
||||||
|
}
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
|
return searchCommon(
|
||||||
|
query,
|
||||||
|
'${await getAPIHost()}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||||
|
'items', onHttpErrorCode: (Response res) {
|
||||||
|
rateLimitErrorCheck(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
rateLimitErrorCheck(Response res) {
|
rateLimitErrorCheck(Response res) {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
throw RateLimitError(
|
throw RateLimitError(
|
||||||
|
@ -1,16 +1,57 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class GitLab extends AppSource {
|
class GitLab extends AppSource {
|
||||||
GitLab() {
|
GitLab() {
|
||||||
host = 'gitlab.com';
|
host = 'gitlab.com';
|
||||||
|
canSearch = true;
|
||||||
|
|
||||||
|
additionalSourceSpecificSettingFormItems = [
|
||||||
|
GeneratedFormTextField('gitlab-creds',
|
||||||
|
label: tr('gitlabPATLabel'),
|
||||||
|
password: true,
|
||||||
|
required: false,
|
||||||
|
belowWidgets: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
tr('about'),
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -19,6 +60,37 @@ class GitLab extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> getPATIfAny() async {
|
||||||
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
|
await settingsProvider.initializeSettings();
|
||||||
|
String? creds = settingsProvider
|
||||||
|
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
|
||||||
|
return creds != null && creds.isNotEmpty ? creds : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
|
String? PAT = await getPATIfAny();
|
||||||
|
if (PAT == null) {
|
||||||
|
throw CredsNeededError(name);
|
||||||
|
}
|
||||||
|
var url =
|
||||||
|
'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}';
|
||||||
|
var res = await sourceRequest(url);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var json = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
Map<String, List<String>> results = {};
|
||||||
|
for (var element in json) {
|
||||||
|
results['https://$host/${element['path_with_namespace']}'] = [
|
||||||
|
element['name_with_namespace'],
|
||||||
|
element['description'] ?? tr('noDescription')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/-/releases';
|
'$standardUrl/-/releases';
|
||||||
@ -28,40 +100,58 @@ class GitLab extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
|
Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var standardUri = Uri.parse(standardUrl);
|
var standardUri = Uri.parse(standardUrl);
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var entry = parsedHtml.querySelector('entry');
|
var apkDetailsList = parsedHtml.querySelectorAll('entry').map((entry) {
|
||||||
var entryContent =
|
var entryContent = parse(
|
||||||
parse(parseFragment(entry?.querySelector('content')!.innerHtml).text);
|
parseFragment(entry.querySelector('content')!.innerHtml).text);
|
||||||
var apkUrls = [
|
var apkUrls = [
|
||||||
...getLinksFromParsedHTML(
|
...getLinksFromParsedHTML(
|
||||||
entryContent,
|
entryContent,
|
||||||
RegExp(
|
RegExp(
|
||||||
'^${standardUri.path.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
'^${standardUri.path.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||||
return '\\${x[0]}';
|
return '\\${x[0]}';
|
||||||
})}/uploads/[^/]+/[^/]+\\.apk\$',
|
})}/uploads/[^/]+/[^/]+\\.apk\$',
|
||||||
caseSensitive: false),
|
caseSensitive: false),
|
||||||
standardUri.origin),
|
standardUri.origin),
|
||||||
// GitLab releases may contain links to externally hosted APKs
|
// GitLab releases may contain links to externally hosted APKs
|
||||||
...getLinksFromParsedHTML(entryContent,
|
...getLinksFromParsedHTML(entryContent,
|
||||||
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
|
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
|
||||||
.where((element) => Uri.parse(element).host != '')
|
.where((element) => Uri.parse(element).host != '')
|
||||||
.toList()
|
.toList()
|
||||||
];
|
];
|
||||||
|
|
||||||
var entryId = entry?.querySelector('id')?.innerHtml;
|
var entryId = entry.querySelector('id')?.innerHtml;
|
||||||
var version =
|
var version =
|
||||||
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
entryId == null ? null : Uri.parse(entryId).pathSegments.last;
|
||||||
var releaseDateString = entry?.querySelector('updated')?.innerHtml;
|
var releaseDateString = entry.querySelector('updated')?.innerHtml;
|
||||||
DateTime? releaseDate =
|
DateTime? releaseDate = releaseDateString != null
|
||||||
releaseDateString != null ? DateTime.parse(releaseDateString) : null;
|
? DateTime.parse(releaseDateString)
|
||||||
if (version == null) {
|
: null;
|
||||||
throw NoVersionError();
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
|
||||||
|
GitHub().getAppNames(standardUrl),
|
||||||
|
releaseDate: releaseDate);
|
||||||
|
});
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, apkUrls, GitHub().getAppNames(standardUrl),
|
if (fallbackToOlderReleases) {
|
||||||
releaseDate: releaseDate);
|
if (additionalSettings['trackOnly'] != true) {
|
||||||
|
apkDetailsList =
|
||||||
|
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||||
|
}
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apkDetailsList.first;
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,96 @@ import 'package:http/http.dart';
|
|||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||||
|
try {
|
||||||
|
Uri.parse(ambiguousUrl).origin;
|
||||||
|
return ambiguousUrl;
|
||||||
|
} catch (err) {
|
||||||
|
// is relative
|
||||||
|
}
|
||||||
|
var currPathSegments = referenceAbsoluteUrl.path
|
||||||
|
.split('/')
|
||||||
|
.where((element) => element.trim().isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
||||||
|
} else if (ambiguousUrl.split('/').length == 1) {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
|
||||||
|
} else {
|
||||||
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareAlphaNumeric(String a, String b) {
|
||||||
|
List<String> aParts = _splitAlphaNumeric(a);
|
||||||
|
List<String> bParts = _splitAlphaNumeric(b);
|
||||||
|
|
||||||
|
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
|
||||||
|
String aPart = aParts[i];
|
||||||
|
String bPart = bParts[i];
|
||||||
|
|
||||||
|
bool aIsNumber = _isNumeric(aPart);
|
||||||
|
bool bIsNumber = _isNumeric(bPart);
|
||||||
|
|
||||||
|
if (aIsNumber && bIsNumber) {
|
||||||
|
int aNumber = int.parse(aPart);
|
||||||
|
int bNumber = int.parse(bPart);
|
||||||
|
int cmp = aNumber.compareTo(bNumber);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
} else if (!aIsNumber && !bIsNumber) {
|
||||||
|
int cmp = aPart.compareTo(bPart);
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Alphanumeric strings come before numeric strings
|
||||||
|
return aIsNumber ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aParts.length.compareTo(bParts.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _splitAlphaNumeric(String s) {
|
||||||
|
List<String> parts = [];
|
||||||
|
StringBuffer sb = StringBuffer();
|
||||||
|
|
||||||
|
bool isNumeric = _isNumeric(s[0]);
|
||||||
|
sb.write(s[0]);
|
||||||
|
|
||||||
|
for (int i = 1; i < s.length; i++) {
|
||||||
|
bool currentIsNumeric = _isNumeric(s[i]);
|
||||||
|
if (currentIsNumeric == isNumeric) {
|
||||||
|
sb.write(s[i]);
|
||||||
|
} else {
|
||||||
|
parts.add(sb.toString());
|
||||||
|
sb.clear();
|
||||||
|
sb.write(s[i]);
|
||||||
|
isNumeric = currentIsNumeric;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.add(sb.toString());
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isNumeric(String s) {
|
||||||
|
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
|
||||||
|
}
|
||||||
|
|
||||||
class HTML extends AppSource {
|
class HTML extends AppSource {
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
// TODO: implement requestHeaders choice, hardcoded for now
|
||||||
|
Map<String, String>? get requestHeaders => {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,14 +103,15 @@ class HTML extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var uri = Uri.parse(standardUrl);
|
var uri = Uri.parse(standardUrl);
|
||||||
Response res = await get(uri);
|
Response res = await sourceRequest(standardUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
List<String> links = parse(res.body)
|
List<String> links = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((element) => element.attributes['href'] ?? '')
|
.map((element) => element.attributes['href'] ?? '')
|
||||||
.where((element) => element.toLowerCase().endsWith('.apk'))
|
.where((element) =>
|
||||||
|
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
|
||||||
.toList();
|
.toList();
|
||||||
links.sort((a, b) => a.split('/').last.compareTo(b.split('/').last));
|
links.sort((a, b) => compareAlphaNumeric(a, b));
|
||||||
if (additionalSettings['apkFilterRegEx'] != null) {
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
links = links.where((element) => reg.hasMatch(element)).toList();
|
links = links.where((element) => reg.hasMatch(element)).toList();
|
||||||
@ -32,17 +120,11 @@ class HTML extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var rel = links.last;
|
var rel = links.last;
|
||||||
var apkName = rel.split('/').last;
|
var version = rel.hashCode.toString();
|
||||||
var version = apkName.substring(0, apkName.length - 4);
|
List<String> apkUrls =
|
||||||
List<String> apkUrls = [rel]
|
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
|
||||||
.map((e) => e.toLowerCase().startsWith('http://') ||
|
return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(),
|
||||||
e.toLowerCase().startsWith('https://')
|
AppNames(uri.host, tr('app')));
|
||||||
? e
|
|
||||||
: e.startsWith('/')
|
|
||||||
? '${uri.origin}/$e'
|
|
||||||
: '${uri.origin}/${uri.path}/$e')
|
|
||||||
.toList();
|
|
||||||
return APKDetails(version, apkUrls, AppNames(uri.host, tr('app')));
|
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -9,7 +8,7 @@ class IzzyOnDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/repo/apk/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -19,8 +18,8 @@ class IzzyOnDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
return FDroid().tryInferringAppId(standardUrl);
|
return FDroid().tryInferringAppId(standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,10 +28,10 @@ class IzzyOnDroid extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await get(
|
await sourceRequest(
|
||||||
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
|
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
|
||||||
'https://android.izzysoft.de/frepo/$appId',
|
'https://android.izzysoft.de/frepo/$appId',
|
||||||
standardUrl);
|
standardUrl);
|
||||||
}
|
}
|
||||||
|
66
lib/app_sources/jenkins.dart
Normal file
66
lib/app_sources/jenkins.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class Jenkins extends AppSource {
|
||||||
|
Jenkins() {
|
||||||
|
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimJobUrl(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('.*/job/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url);
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
|
'$standardUrl/-/releases';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
|
Response res =
|
||||||
|
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var json = jsonDecode(res.body);
|
||||||
|
var releaseDate = json['timestamp'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);
|
||||||
|
var version =
|
||||||
|
json['number'] == null ? null : (json['number'] as int).toString();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
var apkUrls = (json['artifacts'] as List<dynamic>)
|
||||||
|
.map((e) {
|
||||||
|
var path = (e['relativePath'] as String?);
|
||||||
|
if (path != null && path.isNotEmpty) {
|
||||||
|
path = '$standardUrl/lastSuccessfulBuild/artifact/$path';
|
||||||
|
}
|
||||||
|
return path == null
|
||||||
|
? const MapEntry<String, String>('', '')
|
||||||
|
: MapEntry<String, String>(
|
||||||
|
(e['fileName'] ?? e['relativePath']) as String, path);
|
||||||
|
})
|
||||||
|
.where((url) =>
|
||||||
|
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
return APKDetails(
|
||||||
|
version,
|
||||||
|
apkUrls,
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
AppNames(Uri.parse(standardUrl).host, standardUrl.split('/').last));
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ class Mullvad extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
RegExp standardUrlRegEx = RegExp('^https?://$host');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -29,24 +28,41 @@ class Mullvad extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
var details = await HTML().getLatestAPKDetails(
|
Response res = await sourceRequest('$standardUrl/en/download/android');
|
||||||
'$standardUrl/en/download/android', additionalSettings);
|
if (res.statusCode == 200) {
|
||||||
var fileName = details.apkUrls[0].split('/').last;
|
var versions = parse(res.body)
|
||||||
var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(fileName);
|
.querySelectorAll('p')
|
||||||
if (versionMatch == null) {
|
.map((e) => e.innerHtml)
|
||||||
throw NoVersionError();
|
.where((p) => p.contains('Latest version: '))
|
||||||
|
.map((e) {
|
||||||
|
var match = RegExp('[0-9]+(\\.[0-9]+)*').firstMatch(e);
|
||||||
|
if (match == null) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return e.substring(match.start, match.end);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((element) => element.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (versions.isEmpty) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? changeLog;
|
||||||
|
try {
|
||||||
|
changeLog = (await GitHub().getLatestAPKDetails(
|
||||||
|
'https://github.com/mullvad/mullvadvpn-app',
|
||||||
|
{'fallbackToOlderReleases': true}))
|
||||||
|
.changeLog;
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
versions[0],
|
||||||
|
getApkUrlsFromUrls(['https://mullvad.net/download/app/apk/latest']),
|
||||||
|
AppNames(name, 'Mullvad-VPN'),
|
||||||
|
changeLog: changeLog);
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
details.version = fileName.substring(versionMatch.start, versionMatch.end);
|
|
||||||
details.names = AppNames(name, 'Mullvad-VPN');
|
|
||||||
try {
|
|
||||||
details.changeLog = (await GitHub().getLatestAPKDetails(
|
|
||||||
'https://github.com/mullvad/mullvadvpn-app',
|
|
||||||
{'fallbackToOlderReleases': true}))
|
|
||||||
.changeLog;
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
return details;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ class NeutronCode extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
|
RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
@ -78,7 +78,7 @@ class NeutronCode extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse(standardUrl));
|
Response res = await sourceRequest(standardUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var name = http.querySelector('.pd-title')?.innerHtml;
|
var name = http.querySelector('.pd-title')?.innerHtml;
|
||||||
@ -98,7 +98,7 @@ class NeutronCode extends AppSource {
|
|||||||
? (customDateParse(dateStringOriginal))
|
? (customDateParse(dateStringOriginal))
|
||||||
: null;
|
: null;
|
||||||
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
var changeLogElements = http.querySelectorAll('.pd-fdesc p');
|
||||||
return APKDetails(version, [apkUrl],
|
return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
|
||||||
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last),
|
||||||
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
releaseDate: dateString != null ? DateTime.parse(dateString) : null,
|
||||||
changeLog: changeLogElements.isNotEmpty
|
changeLog: changeLogElements.isNotEmpty
|
||||||
|
@ -9,7 +9,7 @@ class Signal extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class Signal extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res =
|
Response res =
|
||||||
await get(Uri.parse('https://updates.$host/android/latest.json'));
|
await sourceRequest('https://updates.$host/android/latest.json');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
String? apkUrl = json['url'];
|
String? apkUrl = json['url'];
|
||||||
@ -28,7 +28,8 @@ class Signal extends AppSource {
|
|||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
return APKDetails(version, apkUrls, AppNames(name, 'Signal'));
|
return APKDetails(
|
||||||
|
version, getApkUrlsFromUrls(apkUrls), AppNames(name, 'Signal'));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,15 @@ class SourceForge extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
|
RegExp standardUrlRegExB = RegExp('^https?://$host/p/[^/]+');
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
|
if (match != null) {
|
||||||
|
url =
|
||||||
|
'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
|
||||||
|
}
|
||||||
|
RegExp standardUrlRegExA = RegExp('^https?://$host/projects/[^/]+');
|
||||||
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw InvalidURLError(name);
|
throw InvalidURLError(name);
|
||||||
}
|
}
|
||||||
@ -23,7 +29,7 @@ class SourceForge extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
|
Response res = await sourceRequest('$standardUrl/rss?path=/');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var parsedHtml = parse(res.body);
|
var parsedHtml = parse(res.body);
|
||||||
var allDownloadLinks =
|
var allDownloadLinks =
|
||||||
@ -31,7 +37,8 @@ class SourceForge extends AppSource {
|
|||||||
getVersion(String url) {
|
getVersion(String url) {
|
||||||
try {
|
try {
|
||||||
var tokens = url.split('/');
|
var tokens = url.split('/');
|
||||||
return tokens[tokens.length - 3];
|
var fi = tokens.indexOf('files');
|
||||||
|
return tokens[tokens[fi + 2] == 'download' ? fi - 1 : fi + 1];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -50,7 +57,7 @@ class SourceForge extends AppSource {
|
|||||||
.toList();
|
.toList();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrlList,
|
getApkUrlsFromUrls(apkUrlList),
|
||||||
AppNames(
|
AppNames(
|
||||||
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
name, standardUrl.substring(standardUrl.lastIndexOf('/') + 1)));
|
||||||
} else {
|
} else {
|
||||||
|
107
lib/app_sources/sourcehut.dart
Normal file
107
lib/app_sources/sourcehut.dart
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
|
class SourceHut extends AppSource {
|
||||||
|
SourceHut() {
|
||||||
|
host = 'git.sr.ht';
|
||||||
|
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||||
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
Uri standardUri = Uri.parse(standardUrl);
|
||||||
|
String appName = standardUri.pathSegments.last;
|
||||||
|
bool fallbackToOlderReleases =
|
||||||
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
|
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var parsedHtml = parse(res.body);
|
||||||
|
List<APKDetails> apkDetailsList = [];
|
||||||
|
int ind = 0;
|
||||||
|
|
||||||
|
for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) {
|
||||||
|
// Limit 5 for speed
|
||||||
|
if (!fallbackToOlderReleases && ind > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String? version = entry.querySelector('title')?.text.trim();
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
String? releaseDateString = entry.querySelector('pubDate')?.innerHtml;
|
||||||
|
String releasePage = '$standardUrl/refs/$version';
|
||||||
|
DateTime? releaseDate;
|
||||||
|
try {
|
||||||
|
releaseDate = releaseDateString != null
|
||||||
|
? DateFormat('E, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||||
|
: null;
|
||||||
|
releaseDate = releaseDateString != null
|
||||||
|
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z')
|
||||||
|
.parse(releaseDateString)
|
||||||
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
var res2 = await sourceRequest(releasePage);
|
||||||
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
|
if (res2.statusCode == 200) {
|
||||||
|
apkUrls = getApkUrlsFromUrls(parse(res2.body)
|
||||||
|
.querySelectorAll('a')
|
||||||
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
|
.where((e) => e.toLowerCase().endsWith('.apk'))
|
||||||
|
.map((e) => ensureAbsoluteUrl(e, standardUri))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
apkDetailsList.add(APKDetails(
|
||||||
|
version,
|
||||||
|
apkUrls,
|
||||||
|
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
|
||||||
|
appName),
|
||||||
|
releaseDate: releaseDate));
|
||||||
|
ind++;
|
||||||
|
}
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
if (fallbackToOlderReleases) {
|
||||||
|
if (additionalSettings['trackOnly'] != true) {
|
||||||
|
apkDetailsList =
|
||||||
|
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
|
||||||
|
}
|
||||||
|
if (apkDetailsList.isEmpty) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apkDetailsList.first;
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class SteamMobile extends AppSource {
|
|||||||
final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
|
final apks = {'steam': tr('steamMobile'), 'steam-chat-app': tr('steamChat')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ class SteamMobile extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://$host/mobile'));
|
Response res = await sourceRequest('https://$host/mobile');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var apkNamePrefix = additionalSettings['app'] as String?;
|
var apkNamePrefix = additionalSettings['app'] as String?;
|
||||||
if (apkNamePrefix == null) {
|
if (apkNamePrefix == null) {
|
||||||
@ -53,7 +53,8 @@ class SteamMobile extends AppSource {
|
|||||||
var version = links[0].substring(
|
var version = links[0].substring(
|
||||||
versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4);
|
versionMatch.start + apkNamePrefix.length + 2, versionMatch.end - 4);
|
||||||
var apkUrls = [links[0]];
|
var apkUrls = [links[0]];
|
||||||
return APKDetails(version, apkUrls, AppNames(name, apks[apkNamePrefix]!));
|
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
|
||||||
|
AppNames(name, apks[apkNamePrefix]!));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ class TelegramApp extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ class TelegramApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK'));
|
Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var http = parse(res.body);
|
var http = parse(res.body);
|
||||||
var messages =
|
var messages =
|
||||||
@ -32,7 +32,8 @@ class TelegramApp extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
String? apkUrl = 'https://telegram.org/dl/android/apk';
|
||||||
return APKDetails(version, [apkUrl], AppNames('Telegram', 'Telegram'));
|
return APKDetails(version, getApkUrlsFromUrls([apkUrl]),
|
||||||
|
AppNames('Telegram', 'Telegram'));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -10,7 +9,7 @@ class VLC extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,8 +18,8 @@ class VLC extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(
|
Response res = await sourceRequest(
|
||||||
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
|
'https://www.videolan.org/vlc/download-android.html');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var dwUrlBase = 'get.videolan.org/vlc-android';
|
var dwUrlBase = 'get.videolan.org/vlc-android';
|
||||||
var dwLinks = parse(res.body)
|
var dwLinks = parse(res.body)
|
||||||
@ -38,7 +37,7 @@ class VLC extends AppSource {
|
|||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
String? targetUrl = 'https://$dwUrlBase/$version/';
|
String? targetUrl = 'https://$dwUrlBase/$version/';
|
||||||
Response res2 = await get(Uri.parse(targetUrl));
|
Response res2 = await sourceRequest(targetUrl);
|
||||||
String mirrorDwBase =
|
String mirrorDwBase =
|
||||||
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
||||||
List<String> apkUrls = [];
|
List<String> apkUrls = [];
|
||||||
@ -54,7 +53,8 @@ class VLC extends AppSource {
|
|||||||
throw getObtainiumHttpError(res2);
|
throw getObtainiumHttpError(res2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return APKDetails(version, apkUrls, AppNames('VideoLAN', 'VLC'));
|
return APKDetails(
|
||||||
|
version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC'));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -9,26 +9,26 @@ class WhatsApp extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return 'https://$host';
|
return 'https://$host';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
Future<String> apkUrlPrefetchModifier(
|
||||||
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
String apkUrl, String standardUrl) async {
|
||||||
|
Response res = await sourceRequest('https://www.whatsapp.com/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var targetLinks = parse(res.body)
|
var targetLinks = parse(res.body)
|
||||||
.querySelectorAll('a')
|
.querySelectorAll('a')
|
||||||
.map((e) => e.attributes['href'])
|
.map((e) => e.attributes['href'] ?? '')
|
||||||
.where((e) => e != null)
|
.where((e) => e.isNotEmpty)
|
||||||
.where((e) =>
|
.where((e) =>
|
||||||
e!.contains('scontent.whatsapp.net') &&
|
e.contains('content.whatsapp.net') && e.contains('WhatsApp.apk'))
|
||||||
e.contains('WhatsApp.apk'))
|
|
||||||
.toList();
|
.toList();
|
||||||
if (targetLinks.isEmpty) {
|
if (targetLinks.isEmpty) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
return targetLinks[0]!;
|
return targetLinks[0];
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ class WhatsApp extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('https://www.whatsapp.com/android'));
|
Response res = await sourceRequest('https://www.whatsapp.com/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var targetElements = parse(res.body)
|
var targetElements = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
@ -64,9 +64,9 @@ class WhatsApp extends AppSource {
|
|||||||
vLines[0].substring(versionMatch.start, versionMatch.end);
|
vLines[0].substring(versionMatch.start, versionMatch.end);
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
[
|
getApkUrlsFromUrls([
|
||||||
'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
|
'https://www.whatsapp.com/android?v=$version&=thisIsaPlaceholder&a=realURLPrefetchedAtDownloadTime'
|
||||||
],
|
]),
|
||||||
AppNames('Meta', 'WhatsApp'));
|
AppNames('Meta', 'WhatsApp'));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
|
@ -267,7 +267,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
formInputs[r][e] = Row(
|
formInputs[r][e] = Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.items[r][e].label),
|
Flexible(child: Text(widget.items[r][e].label)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
Switch(
|
Switch(
|
||||||
value: values[widget.items[r][e].key],
|
value: values[widget.items[r][e].key],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:android_package_installer/android_package_installer.dart';
|
||||||
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/providers/logs_provider.dart';
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
@ -24,6 +25,11 @@ class InvalidURLError extends ObtainiumError {
|
|||||||
: super(tr('invalidURLForSource', args: [sourceName]));
|
: super(tr('invalidURLForSource', args: [sourceName]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CredsNeededError extends ObtainiumError {
|
||||||
|
CredsNeededError(String sourceName)
|
||||||
|
: super(tr('requiresCredentialsInSettings', args: [sourceName]));
|
||||||
|
}
|
||||||
|
|
||||||
class NoReleasesError extends ObtainiumError {
|
class NoReleasesError extends ObtainiumError {
|
||||||
NoReleasesError() : super(tr('noReleaseFound'));
|
NoReleasesError() : super(tr('noReleaseFound'));
|
||||||
}
|
}
|
||||||
@ -44,8 +50,13 @@ class DowngradeError extends ObtainiumError {
|
|||||||
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
DowngradeError() : super(tr('cantInstallOlderVersion'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InstallError extends ObtainiumError {
|
||||||
|
InstallError(int code)
|
||||||
|
: super(PackageInstallerStatus.byCode(code).name.substring(7));
|
||||||
|
}
|
||||||
|
|
||||||
class IDChangedError extends ObtainiumError {
|
class IDChangedError extends ObtainiumError {
|
||||||
IDChangedError() : super(tr('appIdMismatch'));
|
IDChangedError(String newId) : super('${tr('appIdMismatch')} - $newId');
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotImplementedError extends ObtainiumError {
|
class NotImplementedError extends ObtainiumError {
|
||||||
|
@ -21,21 +21,24 @@ 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.11.17';
|
const String currentVersion = '0.13.14';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
const int bgUpdateCheckAlarmId = 666;
|
const int bgUpdateCheckAlarmId = 666;
|
||||||
|
|
||||||
const supportedLocales = [
|
List<MapEntry<Locale, String>> supportedLocales = const [
|
||||||
Locale('en'),
|
MapEntry(Locale('en'), 'English'),
|
||||||
Locale('zh'),
|
MapEntry(Locale('zh'), '汉语'),
|
||||||
Locale('it'),
|
MapEntry(Locale('it'), 'Italiano'),
|
||||||
Locale('ja'),
|
MapEntry(Locale('ja'), '日本語'),
|
||||||
Locale('hu'),
|
MapEntry(Locale('hu'), 'Magyar'),
|
||||||
Locale('de'),
|
MapEntry(Locale('de'), 'Deutsch'),
|
||||||
Locale('fa'),
|
MapEntry(Locale('fa'), 'فارسی'),
|
||||||
Locale('fr')
|
MapEntry(Locale('fr'), 'Français'),
|
||||||
|
MapEntry(Locale('es'), 'Español'),
|
||||||
|
MapEntry(Locale('pl'), 'Polski'),
|
||||||
|
MapEntry(Locale('ru'), 'Русский язык'),
|
||||||
];
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
@ -52,7 +55,7 @@ Future<void> loadTranslations() async {
|
|||||||
saveLocale: true,
|
saveLocale: true,
|
||||||
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
forceLocale: forceLocale != null ? Locale(forceLocale) : null,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
assetLoader: const RootBundleAssetLoader(),
|
assetLoader: const RootBundleAssetLoader(),
|
||||||
useOnlyLangCode: true,
|
useOnlyLangCode: true,
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
@ -171,7 +174,7 @@ void main() async {
|
|||||||
Provider(create: (context) => LogsProvider())
|
Provider(create: (context) => LogsProvider())
|
||||||
],
|
],
|
||||||
child: EasyLocalization(
|
child: EasyLocalization(
|
||||||
supportedLocales: supportedLocales,
|
supportedLocales: supportedLocales.map((e) => e.key).toList(),
|
||||||
path: localeDir,
|
path: localeDir,
|
||||||
fallbackLocale: fallbackLocale,
|
fallbackLocale: fallbackLocale,
|
||||||
useOnlyLangCode: true,
|
useOnlyLangCode: true,
|
||||||
@ -221,7 +224,7 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
], onlyIfExists: false);
|
], onlyIfExists: false);
|
||||||
}
|
}
|
||||||
if (!supportedLocales
|
if (!supportedLocales
|
||||||
.map((e) => e.languageCode)
|
.map((e) => e.key.languageCode)
|
||||||
.contains(context.locale.languageCode) ||
|
.contains(context.locale.languageCode) ||
|
||||||
settingsProvider.forcedLocale == null &&
|
settingsProvider.forcedLocale == null &&
|
||||||
context.deviceLocale.languageCode !=
|
context.deviceLocale.languageCode !=
|
||||||
@ -263,6 +266,14 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
darkColorScheme = ColorScheme.fromSeed(
|
darkColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: defaultThemeColour, brightness: Brightness.dark);
|
seedColor: defaultThemeColour, brightness: Brightness.dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the background and surface colors to pure black in the amoled theme
|
||||||
|
if (settingsProvider.useBlackTheme) {
|
||||||
|
darkColorScheme = darkColorScheme
|
||||||
|
.copyWith(background: Colors.black, surface: Colors.black)
|
||||||
|
.harmonized();
|
||||||
|
}
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Obtainium',
|
title: 'Obtainium',
|
||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
@ -13,17 +13,20 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
@override
|
@override
|
||||||
late List<String> requiredArgs = [tr('uname')];
|
late List<String> requiredArgs = [tr('uname')];
|
||||||
|
|
||||||
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username, int page) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
urlsWithDescriptions.addAll({
|
urlsWithDescriptions.addAll({
|
||||||
e['html_url'] as String: e['description'] != null
|
e['html_url'] as String: [
|
||||||
? e['description'] as String
|
e['full_name'] as String,
|
||||||
: tr('noDescription')
|
e['description'] != null
|
||||||
|
? e['description'] as String
|
||||||
|
: tr('noDescription')
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return urlsWithDescriptions;
|
return urlsWithDescriptions;
|
||||||
@ -35,11 +38,12 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(
|
||||||
|
List<String> args) async {
|
||||||
if (args.length != requiredArgs.length) {
|
if (args.length != requiredArgs.length) {
|
||||||
throw ObtainiumError(tr('wrongArgNum'));
|
throw ObtainiumError(tr('wrongArgNum'));
|
||||||
}
|
}
|
||||||
Map<String, String> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
var page = 1;
|
var page = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pageUrls =
|
var pageUrls =
|
||||||
|
@ -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/app_sources/html.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/components/generated_form_modal.dart';
|
||||||
@ -28,9 +29,11 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
|
|
||||||
String userInput = '';
|
String userInput = '';
|
||||||
String searchQuery = '';
|
String searchQuery = '';
|
||||||
|
String? pickedSourceOverride;
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
Map<String, dynamic> additionalSettings = {};
|
Map<String, dynamic> additionalSettings = {};
|
||||||
bool additionalSettingsValid = true;
|
bool additionalSettingsValid = true;
|
||||||
|
bool inferAppIdIfOptional = true;
|
||||||
List<String> pickedCategories = [];
|
List<String> pickedCategories = [];
|
||||||
int searchnum = 0;
|
int searchnum = 0;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
@ -49,8 +52,25 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
if (isSearch) {
|
if (isSearch) {
|
||||||
searchnum++;
|
searchnum++;
|
||||||
}
|
}
|
||||||
var source = valid ? sourceProvider.getSource(userInput) : null;
|
var prevHost = pickedSource?.host;
|
||||||
if (pickedSource.runtimeType != source.runtimeType) {
|
try {
|
||||||
|
var naturalSource =
|
||||||
|
valid ? sourceProvider.getSource(userInput) : null;
|
||||||
|
if (naturalSource != null &&
|
||||||
|
naturalSource.runtimeType.toString() !=
|
||||||
|
HTML().runtimeType.toString()) {
|
||||||
|
// If input has changed to match a regular source, reset the override
|
||||||
|
pickedSourceOverride = null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
var source = valid
|
||||||
|
? sourceProvider.getSource(userInput,
|
||||||
|
overrideSource: pickedSourceOverride)
|
||||||
|
: null;
|
||||||
|
if (pickedSource.runtimeType != source.runtimeType ||
|
||||||
|
(prevHost != null && prevHost != source?.host)) {
|
||||||
pickedSource = source;
|
pickedSource = source;
|
||||||
additionalSettings = source != null
|
additionalSettings = source != null
|
||||||
? getDefaultValuesFromFormItems(
|
? getDefaultValuesFromFormItems(
|
||||||
@ -59,29 +79,41 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalSettingsValid = source != null
|
additionalSettingsValid = source != null
|
||||||
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
|
||||||
: true;
|
: true;
|
||||||
|
inferAppIdIfOptional = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async {
|
Future<bool> getTrackOnlyConfirmationIfNeeded(
|
||||||
return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
bool userPickedTrackOnly, SettingsProvider settingsProvider,
|
||||||
// ignore: use_build_context_synchronously
|
{bool ignoreHideSetting = false}) async {
|
||||||
await showDialog(
|
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
|
||||||
context: context,
|
if (useTrackOnly &&
|
||||||
builder: (BuildContext ctx) {
|
(!settingsProvider.hideTrackOnlyWarning || ignoreHideSetting)) {
|
||||||
return GeneratedFormModal(
|
// ignore: use_build_context_synchronously
|
||||||
title: tr('xIsTrackOnly', args: [
|
var values = await showDialog(
|
||||||
pickedSource!.enforceTrackOnly
|
context: context,
|
||||||
? tr('source')
|
builder: (BuildContext ctx) {
|
||||||
: tr('app')
|
return GeneratedFormModal(
|
||||||
]),
|
initValid: true,
|
||||||
items: const [],
|
title: tr('xIsTrackOnly', args: [
|
||||||
message:
|
pickedSource!.enforceTrackOnly ? tr('source') : tr('app')
|
||||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
]),
|
||||||
);
|
items: [
|
||||||
}) ==
|
[GeneratedFormSwitch('hide', label: tr('dontShowAgain'))]
|
||||||
null));
|
],
|
||||||
|
message:
|
||||||
|
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (values != null) {
|
||||||
|
settingsProvider.hideTrackOnlyWarning = values['hide'] == true;
|
||||||
|
}
|
||||||
|
return useTrackOnly && values != null;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getReleaseDateAsVersionConfirmationIfNeeded(
|
getReleaseDateAsVersionConfirmationIfNeeded(
|
||||||
@ -109,16 +141,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
var settingsProvider = context.read<SettingsProvider>();
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||||
App? app;
|
App? app;
|
||||||
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
if ((await getTrackOnlyConfirmationIfNeeded(
|
||||||
|
userPickedTrackOnly, settingsProvider)) &&
|
||||||
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
||||||
userPickedTrackOnly))) {
|
userPickedTrackOnly))) {
|
||||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||||
app = await sourceProvider.getApp(
|
app = await sourceProvider.getApp(
|
||||||
pickedSource!, userInput, additionalSettings,
|
pickedSource!, userInput, additionalSettings,
|
||||||
trackOnlyOverride: trackOnly);
|
trackOnlyOverride: trackOnly,
|
||||||
if (!trackOnly) {
|
overrideSource: pickedSourceOverride,
|
||||||
await settingsProvider.getInstallPermission();
|
inferAppIdIfOptional: inferAppIdIfOptional);
|
||||||
}
|
|
||||||
// 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) &&
|
if (sourceProvider.isTempId(app) &&
|
||||||
app.additionalSettings['trackOnly'] != true) {
|
app.additionalSettings['trackOnly'] != true) {
|
||||||
@ -127,16 +159,26 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
if (apkUrl == null) {
|
if (apkUrl == null) {
|
||||||
throw ObtainiumError(tr('cancelled'));
|
throw ObtainiumError(tr('cancelled'));
|
||||||
}
|
}
|
||||||
app.preferredApkIndex = app.apkUrls.indexOf(apkUrl);
|
app.preferredApkIndex =
|
||||||
|
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedApk = await appsProvider.downloadApp(
|
var downloadedArtifact = await appsProvider.downloadApp(
|
||||||
app, globalNavigatorKey.currentContext);
|
app, globalNavigatorKey.currentContext);
|
||||||
app.id = downloadedApk.appId;
|
DownloadedApk? downloadedFile;
|
||||||
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
app.id = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||||
}
|
}
|
||||||
if (appsProvider.apps.containsKey(app.id)) {
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
throw ObtainiumError(tr('appAlreadyAdded'));
|
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||||
}
|
}
|
||||||
if (app.additionalSettings['trackOnly'] == true) {
|
if (app.additionalSettings['trackOnly'] == true ||
|
||||||
|
app.additionalSettings['versionDetection'] !=
|
||||||
|
'standardVersionDetection') {
|
||||||
app.installedVersion = app.latestVersion;
|
app.installedVersion = app.latestVersion;
|
||||||
}
|
}
|
||||||
app.categories = pickedCategories;
|
app.categories = pickedCategories;
|
||||||
@ -172,9 +214,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
(value) {
|
(value) {
|
||||||
try {
|
try {
|
||||||
sourceProvider
|
sourceProvider
|
||||||
.getSource(value ?? '')
|
.getSource(value ?? '',
|
||||||
.standardizeURL(
|
overrideSource: pickedSourceOverride)
|
||||||
preStandardizeUrl(value ?? ''));
|
.standardizeUrl(value ?? '');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e is String
|
return e is String
|
||||||
? e
|
? e
|
||||||
@ -216,13 +258,22 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
searching = true;
|
searching = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
var results = await Future.wait(sourceProvider.sources
|
var results = await Future.wait(
|
||||||
.where((e) => e.canSearch)
|
sourceProvider.sources.where((e) => e.canSearch).map((e) async {
|
||||||
.map((e) => e.search(searchQuery)));
|
try {
|
||||||
|
return await e.search(searchQuery);
|
||||||
|
} catch (err) {
|
||||||
|
if (err is! CredsNeededError) {
|
||||||
|
rethrow;
|
||||||
|
} else {
|
||||||
|
return <String, List<String>>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// .then((results) async {
|
// .then((results) async {
|
||||||
// Interleave results instead of simple reduce
|
// Interleave results instead of simple reduce
|
||||||
Map<String, String> res = {};
|
Map<String, List<String>> res = {};
|
||||||
var si = 0;
|
var si = 0;
|
||||||
var done = false;
|
var done = false;
|
||||||
while (!done) {
|
while (!done) {
|
||||||
@ -235,6 +286,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
si++;
|
si++;
|
||||||
}
|
}
|
||||||
|
if (res.isEmpty) {
|
||||||
|
throw ObtainiumError(tr('noResults'));
|
||||||
|
}
|
||||||
List<String>? selectedUrls = res.isEmpty
|
List<String>? selectedUrls = res.isEmpty
|
||||||
? []
|
? []
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@ -259,6 +313,48 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget getHTMLSourceOverrideDropdown() => Column(children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormDropdown(
|
||||||
|
'overrideSource',
|
||||||
|
defaultValue: HTML().runtimeType.toString(),
|
||||||
|
[
|
||||||
|
...sourceProvider.sources.map(
|
||||||
|
(s) => MapEntry(s.runtimeType.toString(), s.name))
|
||||||
|
],
|
||||||
|
label: tr('overrideSource'))
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
fn() {
|
||||||
|
pickedSourceOverride = (values['overrideSource'] == null ||
|
||||||
|
values['overrideSource'] == '')
|
||||||
|
? null
|
||||||
|
: values['overrideSource'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBuilding) {
|
||||||
|
setState(() {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
changeUserInput(userInput, valid, isBuilding);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
bool shouldShowSearchBar() =>
|
bool shouldShowSearchBar() =>
|
||||||
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
||||||
pickedSource == null &&
|
pickedSource == null &&
|
||||||
@ -285,26 +381,30 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
searching
|
||||||
onPressed: searchQuery.isEmpty || doingSomething
|
? const CircularProgressIndicator()
|
||||||
? null
|
: ElevatedButton(
|
||||||
: () {
|
onPressed: searchQuery.isEmpty || doingSomething
|
||||||
runSearch();
|
? null
|
||||||
},
|
: () {
|
||||||
child: Text(tr('search')))
|
runSearch();
|
||||||
|
},
|
||||||
|
child: Text(tr('search')))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget getAdditionalOptsCol() => Column(
|
Widget getAdditionalOptsCol() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Divider(
|
const SizedBox(
|
||||||
height: 64,
|
height: 16,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
tr('additionalOptsFor',
|
tr('additionalOptsFor',
|
||||||
args: [pickedSource?.name ?? tr('source')]),
|
args: [pickedSource?.name ?? tr('source')]),
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
@ -331,14 +431,30 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (pickedSource != null && pickedSource!.appIdInferIsOptional)
|
||||||
|
GeneratedForm(
|
||||||
|
key: const Key('inferAppIdIfOptional'),
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('inferAppIdIfOptional',
|
||||||
|
label: tr('tryInferAppIdFromCode'),
|
||||||
|
defaultValue: inferAppIdIfOptional)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: (values, valid, isBuilding) {
|
||||||
|
if (!isBuilding) {
|
||||||
|
setState(() {
|
||||||
|
inferAppIdIfOptional = values['inferAppIdIfOptional'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget getSourcesListWidget() => Expanded(
|
Widget getSourcesListWidget() => Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
),
|
),
|
||||||
@ -365,24 +481,32 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
fontStyle: FontStyle.italic),
|
fontStyle: FontStyle.italic),
|
||||||
)))
|
)))
|
||||||
.toList()
|
.toList()
|
||||||
]));
|
]);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(shrinkWrap: true, slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('addApp')),
|
CustomAppBar(title: tr('addApp')),
|
||||||
SliverFillRemaining(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
getUrlInputRow(),
|
getUrlInputRow(),
|
||||||
if (shouldShowSearchBar())
|
const SizedBox(
|
||||||
const SizedBox(
|
height: 16,
|
||||||
height: 16,
|
),
|
||||||
),
|
if (pickedSourceOverride != null ||
|
||||||
|
(pickedSource != null &&
|
||||||
|
pickedSource.runtimeType.toString() ==
|
||||||
|
HTML().runtimeType.toString()))
|
||||||
|
getHTMLSourceOverrideDropdown(),
|
||||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
if (pickedSource != null)
|
if (pickedSource != null)
|
||||||
getAdditionalOptsCol()
|
getAdditionalOptsCol()
|
||||||
else
|
else
|
||||||
|
@ -32,14 +32,18 @@ class _AppPageState extends State<AppPage> {
|
|||||||
getUpdate(String id) {
|
getUpdate(String id) {
|
||||||
appsProvider.checkUpdate(id).catchError((e) {
|
appsProvider.checkUpdate(id).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
|
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
|
||||||
|
|
||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId]?.deepCopy();
|
||||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
var source = app != null
|
||||||
|
? sourceProvider.getSource(app.app.url,
|
||||||
|
overrideSource: app.app.overrideSource)
|
||||||
|
: null;
|
||||||
if (!areDownloadsRunning && prevApp == null && app != null) {
|
if (!areDownloadsRunning && prevApp == null && app != null) {
|
||||||
prevApp = app;
|
prevApp = app;
|
||||||
getUpdate(app.app.id);
|
getUpdate(app.app.id);
|
||||||
@ -61,6 +65,12 @@ class _AppPageState extends State<AppPage> {
|
|||||||
mode: LaunchMode.externalApplication);
|
mode: LaunchMode.externalApplication);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: app?.app.url ?? ''));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(tr('copiedToClipboard')),
|
||||||
|
));
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
app?.app.url ?? '',
|
app?.app.url ?? '',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -72,20 +82,20 @@ class _AppPageState extends State<AppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
Text(
|
Column(
|
||||||
tr('latestVersionX',
|
children: [
|
||||||
args: [app?.app.latestVersion ?? tr('unknown')]),
|
Text(
|
||||||
textAlign: TextAlign.center,
|
'${tr('latestVersionX', args: [
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
app?.app.latestVersion ?? tr('unknown')
|
||||||
),
|
])}\n${tr('installedVersionX', args: [
|
||||||
Text(
|
app?.app.installedVersion ?? tr('none')
|
||||||
'${tr('installedVersionX', args: [
|
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||||
app?.app.installedVersion ?? tr('none')
|
tr('app')
|
||||||
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
])}' : ''}',
|
||||||
tr('app')
|
textAlign: TextAlign.end,
|
||||||
])}' : ''}',
|
style: Theme.of(context).textTheme.bodyLarge!,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
],
|
||||||
),
|
),
|
||||||
if (app?.app.installedVersion != null &&
|
if (app?.app.installedVersion != null &&
|
||||||
!isVersionDetectionStandard)
|
!isVersionDetectionStandard)
|
||||||
@ -147,7 +157,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
height: 25,
|
height: 25,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
app?.installedInfo?.name ?? app?.app.name ?? tr('app'),
|
app?.name ?? tr('app'),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
),
|
),
|
||||||
@ -262,9 +272,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('additionalOptions'),
|
title: tr('additionalOptions'), items: items);
|
||||||
items: items,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +309,18 @@ class _AppPageState extends State<AppPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getResetInstallStatusButton() => TextButton(
|
||||||
|
onPressed: app?.app == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
app!.app.installedVersion = null;
|
||||||
|
appsProvider.saveApps([app.app]);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
tr('resetInstallStatus'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
));
|
||||||
|
|
||||||
getInstallOrUpdateButton() => TextButton(
|
getInstallOrUpdateButton() => TextButton(
|
||||||
onPressed: (app?.app.installedVersion == null ||
|
onPressed: (app?.app.installedVersion == null ||
|
||||||
app?.app.installedVersion != app?.app.latestVersion) &&
|
app?.app.installedVersion != app?.app.latestVersion) &&
|
||||||
@ -308,11 +328,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? () async {
|
? () async {
|
||||||
try {
|
try {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
if (app?.app.additionalSettings['trackOnly'] != true) {
|
|
||||||
await settingsProvider.getInstallPermission();
|
|
||||||
}
|
|
||||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||||
[app!.app.id], globalNavigatorKey.currentContext);
|
app?.app.id != null ? [app!.app.id] : [],
|
||||||
|
globalNavigatorKey.currentContext);
|
||||||
if (res.isNotEmpty && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
@ -380,7 +398,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
content: getInfoColumn(),
|
content: getInfoColumn(),
|
||||||
title: Text(
|
title: Text(
|
||||||
'${app.app.name} ${tr('byX', args: [
|
'${app.name} ${tr('byX', args: [
|
||||||
app.app.author
|
app.app.author
|
||||||
])}'),
|
])}'),
|
||||||
actions: [
|
actions: [
|
||||||
@ -396,15 +414,23 @@ class _AppPageState extends State<AppPage> {
|
|||||||
icon: const Icon(Icons.more_horiz),
|
icon: const Icon(Icons.more_horiz),
|
||||||
tooltip: tr('more')),
|
tooltip: tr('more')),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(child: getInstallOrUpdateButton()),
|
Expanded(
|
||||||
|
child: (!isVersionDetectionStandard || trackOnly) &&
|
||||||
|
app?.app.installedVersion != null &&
|
||||||
|
app?.app.installedVersion ==
|
||||||
|
app?.app.latestVersion
|
||||||
|
? getResetInstallStatusButton()
|
||||||
|
: getInstallOrUpdateButton()),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
appsProvider.removeAppsWithModal(
|
appsProvider
|
||||||
context, [app!.app]).then((value) {
|
.removeAppsWithModal(
|
||||||
|
context, app != null ? [app.app] : [])
|
||||||
|
.then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
@ -422,7 +448,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: app!.downloadProgress! / 100))
|
value: app!.downloadProgress! >= 0
|
||||||
|
? app.downloadProgress! / 100
|
||||||
|
: null))
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -29,13 +29,13 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
final AppsFilter neutralFilter = AppsFilter();
|
final AppsFilter neutralFilter = AppsFilter();
|
||||||
var updatesOnlyFilter =
|
var updatesOnlyFilter =
|
||||||
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
||||||
Set<App> selectedApps = {};
|
Set<String> selectedAppIds = {};
|
||||||
DateTime? refreshingSince;
|
DateTime? refreshingSince;
|
||||||
|
|
||||||
clearSelected() {
|
clearSelected() {
|
||||||
if (selectedApps.isNotEmpty) {
|
if (selectedAppIds.isNotEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedApps.clear();
|
selectedAppIds.clear();
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -43,33 +43,57 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectThese(List<App> apps) {
|
selectThese(List<App> apps) {
|
||||||
if (selectedApps.isEmpty) {
|
if (selectedAppIds.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var a in apps) {
|
for (var a in apps) {
|
||||||
selectedApps.add(a);
|
selectedAppIds.add(a.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||||
|
GlobalKey<RefreshIndicatorState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
var listedApps = appsProvider.apps.values.toList();
|
var sourceProvider = SourceProvider();
|
||||||
var currentFilterIsUpdatesOnly =
|
var listedApps = appsProvider.getAppValues().toList();
|
||||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
|
||||||
|
|
||||||
selectedApps = selectedApps
|
refresh() {
|
||||||
.where((element) => listedApps.map((e) => e.app).contains(element))
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = DateTime.now();
|
||||||
|
});
|
||||||
|
return appsProvider.checkUpdates().catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
return <App>[];
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appsProvider.loadingApps &&
|
||||||
|
appsProvider.apps.isNotEmpty &&
|
||||||
|
settingsProvider.checkJustStarted() &&
|
||||||
|
settingsProvider.checkOnStart) {
|
||||||
|
_refreshIndicatorKey.currentState?.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAppIds = selectedAppIds
|
||||||
|
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
||||||
.toSet();
|
.toSet();
|
||||||
|
|
||||||
toggleAppSelected(App app) {
|
toggleAppSelected(App app) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedApps.contains(app)) {
|
if (selectedAppIds.map((e) => e).contains(app.id)) {
|
||||||
selectedApps.remove(app);
|
selectedAppIds.removeWhere((a) => a == app.id);
|
||||||
} else {
|
} else {
|
||||||
selectedApps.add(app);
|
selectedAppIds.add(app.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -93,8 +117,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
for (var t in nameTokens) {
|
for (var t in nameTokens) {
|
||||||
var name = app.installedInfo?.name ?? app.app.name;
|
if (!app.name.toLowerCase().contains(t.toLowerCase())) {
|
||||||
if (!name.toLowerCase().contains(t.toLowerCase())) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,23 +127,37 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (filter.idFilter.isNotEmpty) {
|
||||||
|
if (!app.app.id.contains(filter.idFilter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (filter.categoryFilter.isNotEmpty &&
|
if (filter.categoryFilter.isNotEmpty &&
|
||||||
filter.categoryFilter
|
filter.categoryFilter
|
||||||
.intersection(app.app.categories.toSet())
|
.intersection(app.app.categories.toSet())
|
||||||
.isEmpty) {
|
.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (filter.sourceFilter.isNotEmpty &&
|
||||||
|
sourceProvider
|
||||||
|
.getSource(app.app.url,
|
||||||
|
overrideSource: app.app.overrideSource)
|
||||||
|
.runtimeType
|
||||||
|
.toString() !=
|
||||||
|
filter.sourceFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
listedApps.sort((a, b) {
|
listedApps.sort((a, b) {
|
||||||
var nameA = a.installedInfo?.name ?? a.app.name;
|
|
||||||
var nameB = b.installedInfo?.name ?? b.app.name;
|
|
||||||
int result = 0;
|
int result = 0;
|
||||||
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
|
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
|
||||||
result = (a.app.author + nameA).compareTo(b.app.author + nameB);
|
result = ((a.app.author + a.name).toLowerCase())
|
||||||
|
.compareTo((b.app.author + b.name).toLowerCase());
|
||||||
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
||||||
result = (nameA + a.app.author).compareTo(nameB + b.app.author);
|
result = ((a.name + a.app.author).toLowerCase())
|
||||||
|
.compareTo((b.name + b.app.author).toLowerCase());
|
||||||
} else if (settingsProvider.sortColumn ==
|
} else if (settingsProvider.sortColumn ==
|
||||||
SortColumnSettings.releaseDate) {
|
SortColumnSettings.releaseDate) {
|
||||||
result = (a.app.releaseDate)?.compareTo(
|
result = (a.app.releaseDate)?.compareTo(
|
||||||
@ -137,15 +174,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);
|
||||||
|
|
||||||
var existingUpdateIdsAllOrSelected = existingUpdates
|
var existingUpdateIdsAllOrSelected = existingUpdates
|
||||||
.where((element) => selectedApps.isEmpty
|
.where((element) => selectedAppIds.isEmpty
|
||||||
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedApps.map((e) => e.id).contains(element))
|
: selectedAppIds.map((e) => e).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
var newInstallIdsAllOrSelected = appsProvider
|
var newInstallIdsAllOrSelected = appsProvider
|
||||||
.findExistingUpdates(nonInstalledOnly: true)
|
.findExistingUpdates(nonInstalledOnly: true)
|
||||||
.where((element) => selectedApps.isEmpty
|
.where((element) => selectedAppIds.isEmpty
|
||||||
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
? listedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedApps.map((e) => e.id).contains(element))
|
: selectedAppIds.map((e) => e).contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<String> trackOnlyUpdateIdsAllOrSelected = [];
|
List<String> trackOnlyUpdateIdsAllOrSelected = [];
|
||||||
@ -176,6 +213,18 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
listedApps = [...temp, ...listedApps];
|
listedApps = [...temp, ...listedApps];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsProvider.buryNonInstalled) {
|
||||||
|
var temp = [];
|
||||||
|
listedApps = listedApps.where((sa) {
|
||||||
|
if (sa.app.installedVersion == null) {
|
||||||
|
temp.add(sa);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
listedApps = [...listedApps, ...temp];
|
||||||
|
}
|
||||||
|
|
||||||
var tempPinned = [];
|
var tempPinned = [];
|
||||||
var tempNotPinned = [];
|
var tempNotPinned = [];
|
||||||
for (var a in listedApps) {
|
for (var a in listedApps) {
|
||||||
@ -200,12 +249,17 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var listedCategories = getListedCategories();
|
var listedCategories = getListedCategories();
|
||||||
listedCategories.sort((a, b) {
|
listedCategories.sort((a, b) {
|
||||||
return a != null && b != null
|
return a != null && b != null
|
||||||
? a.compareTo(b)
|
? a.toLowerCase().compareTo(b.toLowerCase())
|
||||||
: a == null
|
: a == null
|
||||||
? 1
|
? 1
|
||||||
: -1;
|
: -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Set<App> selectedApps = listedApps
|
||||||
|
.map((e) => e.app)
|
||||||
|
.where((a) => selectedAppIds.contains(a.id))
|
||||||
|
.toSet();
|
||||||
|
|
||||||
showChangeLogDialog(
|
showChangeLogDialog(
|
||||||
String? changesUrl, AppSource appSource, String changeLog, int index) {
|
String? changesUrl, AppSource appSource, String changeLog, int index) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -214,6 +268,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: tr('changes'),
|
title: tr('changes'),
|
||||||
items: const [],
|
items: const [],
|
||||||
|
message: listedApps[index].app.latestVersion,
|
||||||
additionalWidgets: [
|
additionalWidgets: [
|
||||||
changesUrl != null
|
changesUrl != null
|
||||||
? GestureDetector(
|
? GestureDetector(
|
||||||
@ -267,35 +322,37 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
|
|
||||||
getLoadingWidgets() {
|
getLoadingWidgets() {
|
||||||
return [
|
return [
|
||||||
if (appsProvider.loadingApps || listedApps.isEmpty)
|
if (listedApps.isEmpty)
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: appsProvider.loadingApps
|
child: Text(
|
||||||
? const CircularProgressIndicator()
|
appsProvider.apps.isEmpty ? tr('noApps') : tr('noAppsForFilter'),
|
||||||
: Text(
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
appsProvider.apps.isEmpty
|
textAlign: TextAlign.center,
|
||||||
? tr('noApps')
|
))),
|
||||||
: tr('noAppsForFilter'),
|
if (refreshingSince != null || appsProvider.loadingApps)
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
))),
|
|
||||||
if (refreshingSince != null)
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: appsProvider.apps.values
|
value: appsProvider.loadingApps
|
||||||
.where((element) => !(element.app.lastUpdateCheck
|
? null
|
||||||
?.isBefore(refreshingSince!) ??
|
: appsProvider
|
||||||
true))
|
.getAppValues()
|
||||||
.length /
|
.where((element) => !(element.app.lastUpdateCheck
|
||||||
appsProvider.apps.length,
|
?.isBefore(refreshingSince!) ??
|
||||||
|
true))
|
||||||
|
.length /
|
||||||
|
(appsProvider.apps.isNotEmpty
|
||||||
|
? appsProvider.apps.length
|
||||||
|
: 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getChangeLogFn(int appIndex) {
|
getChangeLogFn(int appIndex) {
|
||||||
AppSource appSource =
|
AppSource appSource = SourceProvider().getSource(
|
||||||
SourceProvider().getSource(listedApps[appIndex].app.url);
|
listedApps[appIndex].app.url,
|
||||||
|
overrideSource: listedApps[appIndex].app.overrideSource);
|
||||||
String? changesUrl =
|
String? changesUrl =
|
||||||
appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
|
appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
|
||||||
String? changeLog = listedApps[appIndex].app.changeLog;
|
String? changeLog = listedApps[appIndex].app.changeLog;
|
||||||
@ -326,6 +383,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
[listedApps[appIndex].app.id],
|
[listedApps[appIndex].app.id],
|
||||||
globalNavigatorKey.currentContext).catchError((e) {
|
globalNavigatorKey.currentContext).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
|
return <String>[];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@ -352,7 +410,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
child: Image(
|
child: Image(
|
||||||
image: const AssetImage(
|
image: const AssetImage(
|
||||||
'assets/graphics/icon_small.png'),
|
'assets/graphics/icon_small.png'),
|
||||||
color: Colors.white.withOpacity(0.1),
|
color: Colors.white.withOpacity(0.3),
|
||||||
colorBlendMode: BlendMode.modulate,
|
colorBlendMode: BlendMode.modulate,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
),
|
),
|
||||||
@ -388,36 +446,35 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
width: 10,
|
width: 10,
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
Column(
|
GestureDetector(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
onTap: showChangesFn,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 150),
|
|
||||||
child: Text(
|
|
||||||
getVersionText(index),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
)),
|
|
||||||
]),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
onTap: showChangesFn,
|
Container(
|
||||||
child: Text(
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width / 4),
|
||||||
|
child: Text(getVersionText(index),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end)),
|
||||||
|
]),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
getChangesButtonString(index, showChangesFn != null),
|
getChangesButtonString(index, showChangesFn != null),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
decoration: showChangesFn != null
|
decoration: showChangesFn != null
|
||||||
? TextDecoration.underline
|
? TextDecoration.underline
|
||||||
: TextDecoration.none),
|
: TextDecoration.none),
|
||||||
))
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
))
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -448,7 +505,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.app
|
.app
|
||||||
.categories
|
.categories
|
||||||
.map((e) =>
|
.map((e) =>
|
||||||
Color(settingsProvider.categories[e]!).withAlpha(255))
|
Color(settingsProvider.categories[e] ?? transparent)
|
||||||
|
.withAlpha(255))
|
||||||
.toList(),
|
.toList(),
|
||||||
Color(transparent)
|
Color(transparent)
|
||||||
])),
|
])),
|
||||||
@ -460,15 +518,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.colorScheme
|
.colorScheme
|
||||||
.primary
|
.primary
|
||||||
.withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
|
.withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
|
||||||
selected: selectedApps.contains(listedApps[index].app),
|
selected:
|
||||||
|
selectedAppIds.map((e) => e).contains(listedApps[index].app.id),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
toggleAppSelected(listedApps[index].app);
|
toggleAppSelected(listedApps[index].app);
|
||||||
},
|
},
|
||||||
leading: getAppIcon(index),
|
leading: getAppIcon(index),
|
||||||
title: Text(
|
title: Text(
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
listedApps[index].installedInfo?.name ??
|
listedApps[index].name,
|
||||||
listedApps[index].app.name,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: listedApps[index].app.pinned
|
fontWeight: listedApps[index].app.pinned
|
||||||
@ -484,13 +542,24 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal)),
|
: FontWeight.normal)),
|
||||||
trailing: listedApps[index].downloadProgress != null
|
trailing: listedApps[index].downloadProgress != null
|
||||||
? Text(tr('percentProgress', args: [
|
? SizedBox(
|
||||||
listedApps[index].downloadProgress?.toInt().toString() ??
|
width: 90,
|
||||||
'100'
|
child: Text(
|
||||||
]))
|
listedApps[index].downloadProgress! >= 0
|
||||||
|
? tr('percentProgress', args: [
|
||||||
|
listedApps[index]
|
||||||
|
.downloadProgress!
|
||||||
|
.toInt()
|
||||||
|
.toString()
|
||||||
|
])
|
||||||
|
: tr('pleaseWait'),
|
||||||
|
textAlign: (listedApps[index].downloadProgress! >= 0)
|
||||||
|
? TextAlign.start
|
||||||
|
: TextAlign.end,
|
||||||
|
))
|
||||||
: trailingRow,
|
: trailingRow,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedApps.isNotEmpty) {
|
if (selectedAppIds.isNotEmpty) {
|
||||||
toggleAppSelected(listedApps[index].app);
|
toggleAppSelected(listedApps[index].app);
|
||||||
} else {
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@ -527,7 +596,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSelectAllButton() {
|
getSelectAllButton() {
|
||||||
return selectedApps.isEmpty
|
return selectedAppIds.isEmpty
|
||||||
? TextButton.icon(
|
? TextButton.icon(
|
||||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -541,17 +610,17 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: TextButton.icon(
|
: TextButton.icon(
|
||||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectedApps.isEmpty
|
selectedAppIds.isEmpty
|
||||||
? selectThese(listedApps.map((e) => e.app).toList())
|
? selectThese(listedApps.map((e) => e.app).toList())
|
||||||
: clearSelected();
|
: clearSelected();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
selectedApps.isEmpty
|
selectedAppIds.isEmpty
|
||||||
? Icons.select_all_outlined
|
? Icons.select_all_outlined
|
||||||
: Icons.deselect_outlined,
|
: Icons.deselect_outlined,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
label: Text(selectedApps.length.toString()));
|
label: Text(selectedAppIds.length.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMassObtainFunction() {
|
getMassObtainFunction() {
|
||||||
@ -596,7 +665,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
items: formItems.map((e) => [e]).toList(),
|
items: formItems.map((e) => [e]).toList(),
|
||||||
initValid: true,
|
initValid: true,
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) async {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
if (values.isEmpty) {
|
if (values.isEmpty) {
|
||||||
values = getDefaultValuesFromFormItems([formItems]);
|
values = getDefaultValuesFromFormItems([formItems]);
|
||||||
@ -604,28 +673,23 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
bool shouldInstallUpdates = values['updates'] == true;
|
bool shouldInstallUpdates = values['updates'] == true;
|
||||||
bool shouldInstallNew = values['installs'] == true;
|
bool shouldInstallNew = values['installs'] == true;
|
||||||
bool shouldMarkTrackOnlies = values['trackonlies'] == true;
|
bool shouldMarkTrackOnlies = values['trackonlies'] == true;
|
||||||
(() async {
|
List<String> toInstall = [];
|
||||||
if (shouldInstallNew || shouldInstallUpdates) {
|
if (shouldInstallUpdates) {
|
||||||
await settingsProvider.getInstallPermission();
|
toInstall.addAll(existingUpdateIdsAllOrSelected);
|
||||||
}
|
}
|
||||||
})()
|
if (shouldInstallNew) {
|
||||||
.then((_) {
|
toInstall.addAll(newInstallIdsAllOrSelected);
|
||||||
List<String> toInstall = [];
|
}
|
||||||
if (shouldInstallUpdates) {
|
if (shouldMarkTrackOnlies) {
|
||||||
toInstall.addAll(existingUpdateIdsAllOrSelected);
|
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
|
||||||
}
|
}
|
||||||
if (shouldInstallNew) {
|
appsProvider
|
||||||
toInstall.addAll(newInstallIdsAllOrSelected);
|
.downloadAndInstallLatestApps(
|
||||||
}
|
toInstall, globalNavigatorKey.currentContext,
|
||||||
if (shouldMarkTrackOnlies) {
|
settingsProvider: settingsProvider)
|
||||||
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
|
.catchError((e) {
|
||||||
}
|
showError(e, context);
|
||||||
appsProvider
|
return <String>[];
|
||||||
.downloadAndInstallLatestApps(
|
|
||||||
toInstall, globalNavigatorKey.currentContext)
|
|
||||||
.catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -699,7 +763,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(tr('markXSelectedAppsAsUpdated',
|
title: Text(tr('markXSelectedAppsAsUpdated',
|
||||||
args: [selectedApps.length.toString()])),
|
args: [selectedAppIds.length.toString()])),
|
||||||
content: Text(
|
content: Text(
|
||||||
tr('onlyWorksWithNonVersionDetectApps'),
|
tr('onlyWorksWithNonVersionDetectApps'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -734,40 +798,36 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pinSelectedApps() {
|
pinSelectedApps() {
|
||||||
() {
|
var pinStatus = selectedApps.where((element) => element.pinned).isEmpty;
|
||||||
var pinStatus = selectedApps.where((element) => element.pinned).isEmpty;
|
appsProvider.saveApps(selectedApps.map((e) {
|
||||||
appsProvider.saveApps(selectedApps.map((e) {
|
e.pinned = pinStatus;
|
||||||
e.pinned = pinStatus;
|
return e;
|
||||||
return e;
|
}).toList());
|
||||||
}).toList());
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetSelectedAppsInstallStatuses() {
|
resetSelectedAppsInstallStatuses() async {
|
||||||
() async {
|
try {
|
||||||
try {
|
var values = await showDialog(
|
||||||
var values = await showDialog(
|
context: context,
|
||||||
context: context,
|
builder: (BuildContext ctx) {
|
||||||
builder: (BuildContext ctx) {
|
return GeneratedFormModal(
|
||||||
return GeneratedFormModal(
|
title: tr('resetInstallStatusForSelectedAppsQuestion'),
|
||||||
title: tr('resetInstallStatusForSelectedAppsQuestion'),
|
items: const [],
|
||||||
items: const [],
|
initValid: true,
|
||||||
initValid: true,
|
message: tr('installStatusOfXWillBeResetExplanation',
|
||||||
message: tr('installStatusOfXWillBeResetExplanation',
|
args: [plural('app', selectedAppIds.length)]),
|
||||||
args: [plural('app', selectedApps.length)]),
|
);
|
||||||
);
|
});
|
||||||
});
|
if (values != null) {
|
||||||
if (values != null) {
|
appsProvider.saveApps(selectedApps.map((e) {
|
||||||
appsProvider.saveApps(selectedApps.map((e) {
|
e.installedVersion = null;
|
||||||
e.installedVersion = null;
|
return e;
|
||||||
return e;
|
}).toList());
|
||||||
}).toList());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
}
|
||||||
};
|
} finally {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showMoreOptionsDialog() {
|
showMoreOptionsDialog() {
|
||||||
@ -815,7 +875,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: resetSelectedAppsInstallStatuses(),
|
onPressed: resetSelectedAppsInstallStatuses,
|
||||||
tooltip: tr('resetInstallStatus'),
|
tooltip: tr('resetInstallStatus'),
|
||||||
icon: const Icon(Icons.restore_page_outlined),
|
icon: const Icon(Icons.restore_page_outlined),
|
||||||
),
|
),
|
||||||
@ -825,44 +885,41 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainBottomButtonsRow() {
|
getMainBottomButtons() {
|
||||||
return Row(
|
return [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
IconButton(
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedApps.isEmpty
|
onPressed: getMassObtainFunction(),
|
||||||
? null
|
tooltip: selectedAppIds.isEmpty
|
||||||
: () {
|
? tr('installUpdateApps')
|
||||||
appsProvider.removeAppsWithModal(
|
: tr('installUpdateSelectedApps'),
|
||||||
context, selectedApps.toList());
|
icon: const Icon(
|
||||||
},
|
Icons.file_download_outlined,
|
||||||
tooltip: tr('removeSelectedApps'),
|
)),
|
||||||
icon: const Icon(Icons.delete_outline_outlined),
|
IconButton(
|
||||||
),
|
visualDensity: VisualDensity.compact,
|
||||||
IconButton(
|
onPressed: selectedAppIds.isEmpty
|
||||||
visualDensity: VisualDensity.compact,
|
? null
|
||||||
onPressed: getMassObtainFunction(),
|
: () {
|
||||||
tooltip: selectedApps.isEmpty
|
appsProvider.removeAppsWithModal(
|
||||||
? tr('installUpdateApps')
|
context, selectedApps.toList());
|
||||||
: tr('installUpdateSelectedApps'),
|
},
|
||||||
icon: const Icon(
|
tooltip: tr('removeSelectedApps'),
|
||||||
Icons.file_download_outlined,
|
icon: const Icon(Icons.delete_outline_outlined),
|
||||||
)),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedApps.isEmpty ? null : launchCategorizeDialog(),
|
onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
|
||||||
tooltip: tr('categorize'),
|
tooltip: tr('categorize'),
|
||||||
icon: const Icon(Icons.category_outlined),
|
icon: const Icon(Icons.category_outlined),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: selectedApps.isEmpty ? null : showMoreOptionsDialog,
|
onPressed: selectedAppIds.isEmpty ? null : showMoreOptionsDialog,
|
||||||
tooltip: tr('more'),
|
tooltip: tr('more'),
|
||||||
icon: const Icon(Icons.more_horiz),
|
icon: const Icon(Icons.more_horiz),
|
||||||
),
|
),
|
||||||
],
|
];
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showFilterDialog() async {
|
showFilterDialog() async {
|
||||||
@ -884,6 +941,12 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
required: false,
|
required: false,
|
||||||
defaultValue: vals['author'])
|
defaultValue: vals['author'])
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('appId',
|
||||||
|
label: tr('appId'),
|
||||||
|
required: false,
|
||||||
|
defaultValue: vals['appId'])
|
||||||
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch('upToDateApps',
|
GeneratedFormSwitch('upToDateApps',
|
||||||
label: tr('upToDateApps'),
|
label: tr('upToDateApps'),
|
||||||
@ -893,6 +956,19 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
GeneratedFormSwitch('nonInstalledApps',
|
GeneratedFormSwitch('nonInstalledApps',
|
||||||
label: tr('nonInstalledApps'),
|
label: tr('nonInstalledApps'),
|
||||||
defaultValue: vals['nonInstalledApps'])
|
defaultValue: vals['nonInstalledApps'])
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormDropdown(
|
||||||
|
'sourceFilter',
|
||||||
|
label: tr('appSource'),
|
||||||
|
defaultValue: filter.sourceFilter,
|
||||||
|
[
|
||||||
|
MapEntry('', tr('none')),
|
||||||
|
...sourceProvider.sources
|
||||||
|
.map((e) =>
|
||||||
|
MapEntry(e.runtimeType.toString(), e.name))
|
||||||
|
.toList()
|
||||||
|
])
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
additionalWidgets: [
|
additionalWidgets: [
|
||||||
@ -916,50 +992,33 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilterButtonsRow() {
|
getFilterButtonsRow() {
|
||||||
|
var isFilterOff = filter.isIdenticalTo(neutralFilter, settingsProvider);
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
getSelectAllButton(),
|
getSelectAllButton(),
|
||||||
|
IconButton(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||||
|
tooltip: isFilterOff ? tr('filter') : tr('filterActive'),
|
||||||
|
onPressed: isFilterOff
|
||||||
|
? showFilterDialog
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
filter = AppsFilter();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(isFilterOff
|
||||||
|
? Icons.filter_list_rounded
|
||||||
|
: Icons.filter_list_off_rounded)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Row(
|
||||||
scrollDirection: Axis.horizontal,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
child: getMainBottomButtonsRow())),
|
children: getMainBottomButtons(),
|
||||||
const VerticalDivider(),
|
)),
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
if (currentFilterIsUpdatesOnly) {
|
|
||||||
filter = AppsFilter();
|
|
||||||
} else {
|
|
||||||
filter = updatesOnlyFilter;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: currentFilterIsUpdatesOnly
|
|
||||||
? tr('removeOutdatedFilter')
|
|
||||||
: tr('showOutdatedOnly'),
|
|
||||||
icon: Icon(
|
|
||||||
currentFilterIsUpdatesOnly
|
|
||||||
? Icons.update_disabled_rounded
|
|
||||||
: Icons.update_rounded,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton.icon(
|
|
||||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
|
||||||
label: Text(
|
|
||||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
|
||||||
? tr('filter')
|
|
||||||
: tr('filterActive'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight:
|
|
||||||
filter.isIdenticalTo(neutralFilter, settingsProvider)
|
|
||||||
? FontWeight.normal
|
|
||||||
: FontWeight.bold),
|
|
||||||
),
|
|
||||||
onPressed: showFilterDialog,
|
|
||||||
icon: const Icon(Icons.filter_list_rounded))
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -983,19 +1042,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () {
|
key: _refreshIndicatorKey,
|
||||||
HapticFeedback.lightImpact();
|
onRefresh: refresh,
|
||||||
setState(() {
|
|
||||||
refreshingSince = DateTime.now();
|
|
||||||
});
|
|
||||||
return appsProvider.checkUpdates().catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
refreshingSince = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: CustomScrollView(slivers: <Widget>[
|
child: CustomScrollView(slivers: <Widget>[
|
||||||
CustomAppBar(title: tr('appsString')),
|
CustomAppBar(title: tr('appsString')),
|
||||||
...getLoadingWidgets(),
|
...getLoadingWidgets(),
|
||||||
@ -1013,37 +1061,47 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
class AppsFilter {
|
class AppsFilter {
|
||||||
late String nameFilter;
|
late String nameFilter;
|
||||||
late String authorFilter;
|
late String authorFilter;
|
||||||
|
late String idFilter;
|
||||||
late bool includeUptodate;
|
late bool includeUptodate;
|
||||||
late bool includeNonInstalled;
|
late bool includeNonInstalled;
|
||||||
late Set<String> categoryFilter;
|
late Set<String> categoryFilter;
|
||||||
|
late String sourceFilter;
|
||||||
|
|
||||||
AppsFilter(
|
AppsFilter(
|
||||||
{this.nameFilter = '',
|
{this.nameFilter = '',
|
||||||
this.authorFilter = '',
|
this.authorFilter = '',
|
||||||
|
this.idFilter = '',
|
||||||
this.includeUptodate = true,
|
this.includeUptodate = true,
|
||||||
this.includeNonInstalled = true,
|
this.includeNonInstalled = true,
|
||||||
this.categoryFilter = const {}});
|
this.categoryFilter = const {},
|
||||||
|
this.sourceFilter = ''});
|
||||||
|
|
||||||
Map<String, dynamic> toFormValuesMap() {
|
Map<String, dynamic> toFormValuesMap() {
|
||||||
return {
|
return {
|
||||||
'appName': nameFilter,
|
'appName': nameFilter,
|
||||||
'author': authorFilter,
|
'author': authorFilter,
|
||||||
|
'appId': idFilter,
|
||||||
'upToDateApps': includeUptodate,
|
'upToDateApps': includeUptodate,
|
||||||
'nonInstalledApps': includeNonInstalled
|
'nonInstalledApps': includeNonInstalled,
|
||||||
|
'sourceFilter': sourceFilter
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormValuesFromMap(Map<String, dynamic> values) {
|
setFormValuesFromMap(Map<String, dynamic> values) {
|
||||||
nameFilter = values['appName']!;
|
nameFilter = values['appName']!;
|
||||||
authorFilter = values['author']!;
|
authorFilter = values['author']!;
|
||||||
|
idFilter = values['appId']!;
|
||||||
includeUptodate = values['upToDateApps'];
|
includeUptodate = values['upToDateApps'];
|
||||||
includeNonInstalled = values['nonInstalledApps'];
|
includeNonInstalled = values['nonInstalledApps'];
|
||||||
|
sourceFilter = values['sourceFilter'];
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
|
bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
|
||||||
authorFilter.trim() == other.authorFilter.trim() &&
|
authorFilter.trim() == other.authorFilter.trim() &&
|
||||||
nameFilter.trim() == other.nameFilter.trim() &&
|
nameFilter.trim() == other.nameFilter.trim() &&
|
||||||
|
idFilter.trim() == other.idFilter.trim() &&
|
||||||
includeUptodate == other.includeUptodate &&
|
includeUptodate == other.includeUptodate &&
|
||||||
includeNonInstalled == other.includeNonInstalled &&
|
includeNonInstalled == other.includeNonInstalled &&
|
||||||
settingsProvider.setEqual(categoryFilter, other.categoryFilter);
|
settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
|
||||||
|
sourceFilter.trim() == other.sourceFilter.trim();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import 'package:obtainium/pages/add_app.dart';
|
|||||||
import 'package:obtainium/pages/apps.dart';
|
import 'package:obtainium/pages/apps.dart';
|
||||||
import 'package:obtainium/pages/import_export.dart';
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
import 'package:obtainium/pages/settings.dart';
|
import 'package:obtainium/pages/settings.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
@ -24,6 +26,8 @@ class NavigationPageItem {
|
|||||||
|
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
List<int> selectedIndexHistory = [];
|
List<int> selectedIndexHistory = [];
|
||||||
|
int prevAppCount = -1;
|
||||||
|
bool prevIsLoading = true;
|
||||||
|
|
||||||
List<NavigationPageItem> pages = [
|
List<NavigationPageItem> pages = [
|
||||||
NavigationPageItem(tr('appsString'), Icons.apps,
|
NavigationPageItem(tr('appsString'), Icons.apps,
|
||||||
@ -36,6 +40,41 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
AppsProvider appsProvider = context.watch<AppsProvider>();
|
||||||
|
|
||||||
|
switchToPage(int index) async {
|
||||||
|
if (index == 0) {
|
||||||
|
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
|
||||||
|
null) {
|
||||||
|
// Avoid duplicate GlobalKey error
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
selectedIndexHistory.clear();
|
||||||
|
});
|
||||||
|
} else if (selectedIndexHistory.isEmpty ||
|
||||||
|
(selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last != index)) {
|
||||||
|
setState(() {
|
||||||
|
int existingInd = selectedIndexHistory.indexOf(index);
|
||||||
|
if (existingInd >= 0) {
|
||||||
|
selectedIndexHistory.removeAt(existingInd);
|
||||||
|
}
|
||||||
|
selectedIndexHistory.add(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prevIsLoading &&
|
||||||
|
prevAppCount >= 0 &&
|
||||||
|
appsProvider.apps.length > prevAppCount &&
|
||||||
|
selectedIndexHistory.isNotEmpty &&
|
||||||
|
selectedIndexHistory.last == 1) {
|
||||||
|
switchToPage(0);
|
||||||
|
}
|
||||||
|
prevAppCount = appsProvider.apps.length;
|
||||||
|
prevIsLoading = appsProvider.loadingApps;
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@ -65,27 +104,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (int index) async {
|
onDestinationSelected: (int index) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
if (index == 0) {
|
await switchToPage(index);
|
||||||
while ((pages[0].widget.key as GlobalKey<AppsPageState>)
|
|
||||||
.currentState !=
|
|
||||||
null) {
|
|
||||||
// Avoid duplicate GlobalKey error
|
|
||||||
await Future.delayed(const Duration(microseconds: 1));
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
selectedIndexHistory.clear();
|
|
||||||
});
|
|
||||||
} else if (selectedIndexHistory.isEmpty ||
|
|
||||||
(selectedIndexHistory.isNotEmpty &&
|
|
||||||
selectedIndexHistory.last != index)) {
|
|
||||||
setState(() {
|
|
||||||
int existingInd = selectedIndexHistory.indexOf(index);
|
|
||||||
if (existingInd >= 0) {
|
|
||||||
selectedIndexHistory.removeAt(existingInd);
|
|
||||||
}
|
|
||||||
selectedIndexHistory.add(index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last,
|
||||||
|
@ -133,7 +133,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
settingsProvider.categories = cats;
|
appsProvider.addMissingCategories(settingsProvider);
|
||||||
showError(tr('importedX', args: [plural('apps', value)]), context);
|
showError(tr('importedX', args: [plural('apps', value)]), context);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -323,8 +323,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (importInProgress)
|
if (importInProgress)
|
||||||
Column(
|
const Column(
|
||||||
children: const [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 14,
|
height: 14,
|
||||||
),
|
),
|
||||||
@ -470,7 +470,7 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
this.selectedByDefault = true,
|
this.selectedByDefault = true,
|
||||||
this.onlyOneSelectionAllowed = false});
|
this.onlyOneSelectionAllowed = false});
|
||||||
|
|
||||||
Map<String, String> urlsWithDescriptions;
|
Map<String, List<String>> urlsWithDescriptions;
|
||||||
bool selectedByDefault;
|
bool selectedByDefault;
|
||||||
bool onlyOneSelectionAllowed;
|
bool onlyOneSelectionAllowed;
|
||||||
|
|
||||||
@ -479,7 +479,7 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||||
Map<MapEntry<String, String>, bool> urlWithDescriptionSelections = {};
|
Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {};
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -506,7 +506,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||||
select(bool? value) {
|
selectThis(bool? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
value ??= false;
|
value ??= false;
|
||||||
if (value! && widget.onlyOneSelectionAllowed) {
|
if (value! && widget.onlyOneSelectionAllowed) {
|
||||||
@ -517,11 +517,68 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Row(children: [
|
var urlLink = GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(urlWithD.key,
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
urlWithD.value[0],
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
Uri.parse(urlWithD.key).host,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
var descriptionText = Text(
|
||||||
|
urlWithD.value[1].length > 128
|
||||||
|
? '${urlWithD.value[1].substring(0, 128)}...'
|
||||||
|
: urlWithD.value[1],
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
var selectedUrlsWithDs = urlWithDescriptionSelections.entries
|
||||||
|
.where((e) => e.value)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var singleSelectTile = ListTile(
|
||||||
|
title: urlLink,
|
||||||
|
subtitle: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectOnlyOne(urlWithD.key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: descriptionText,
|
||||||
|
),
|
||||||
|
leading: Radio<String>(
|
||||||
|
value: urlWithD.key,
|
||||||
|
groupValue: selectedUrlsWithDs.isEmpty
|
||||||
|
? null
|
||||||
|
: selectedUrlsWithDs.first.key.key,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
selectOnlyOne(urlWithD.key);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var multiSelectTile = Row(children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: urlWithDescriptionSelections[urlWithD],
|
value: urlWithDescriptionSelections[urlWithD],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
select(value);
|
selectThis(value);
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@ -534,28 +591,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
urlLink,
|
||||||
onTap: () {
|
|
||||||
launchUrlString(urlWithD.key,
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
Uri.parse(urlWithD.key).path.substring(1),
|
|
||||||
style:
|
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
)),
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
select(!(urlWithDescriptionSelections[urlWithD] ?? false));
|
selectThis(
|
||||||
|
!(urlWithDescriptionSelections[urlWithD] ?? false));
|
||||||
},
|
},
|
||||||
child: Text(
|
child: descriptionText,
|
||||||
urlWithD.value.length > 128
|
|
||||||
? '${urlWithD.value.substring(0, 128)}...'
|
|
||||||
: urlWithD.value,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
@ -563,6 +605,10 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
],
|
],
|
||||||
))
|
))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return widget.onlyOneSelectionAllowed
|
||||||
|
? singleSelectTile
|
||||||
|
: multiSelectTile;
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart';
|
|||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
|
import 'package:obtainium/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';
|
||||||
@ -143,8 +144,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
child: Text(tr('followSystem')),
|
child: Text(tr('followSystem')),
|
||||||
),
|
),
|
||||||
...supportedLocales.map((e) => DropdownMenuItem(
|
...supportedLocales.map((e) => DropdownMenuItem(
|
||||||
value: e.toLanguageTag(),
|
value: e.key.toLanguageTag(),
|
||||||
child: Text(e.toLanguageTag().toUpperCase()),
|
child: Text(e.value),
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -204,6 +205,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const height32 = SizedBox(
|
||||||
|
height: 32,
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@ -216,13 +221,53 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
: Column(
|
: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text(
|
||||||
|
tr('updates'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
intervalDropdown,
|
||||||
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('checkOnStart'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.checkOnStart,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.checkOnStart = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height32,
|
||||||
|
Text(
|
||||||
|
tr('sourceSpecific'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
...sourceSpecificFields,
|
||||||
|
height32,
|
||||||
Text(
|
Text(
|
||||||
tr('appearance'),
|
tr('appearance'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
themeDropdown,
|
themeDropdown,
|
||||||
height16,
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('useBlackTheme'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.useBlackTheme,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.useBlackTheme = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
colourDropdown,
|
colourDropdown,
|
||||||
height16,
|
height16,
|
||||||
Row(
|
Row(
|
||||||
@ -242,7 +287,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('showWebInAppView')),
|
Flexible(child: Text(tr('showWebInAppView'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.showAppWebpage,
|
value: settingsProvider.showAppWebpage,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -254,7 +299,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('pinUpdates')),
|
Flexible(child: Text(tr('pinUpdates'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.pinUpdates,
|
value: settingsProvider.pinUpdates,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -266,7 +311,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('groupByCategory')),
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
tr('moveNonInstalledAppsToBottom'))),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.buryNonInstalled,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.buryNonInstalled = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(child: Text(tr('groupByCategory'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.groupByCategory,
|
value: settingsProvider.groupByCategory,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -274,31 +333,43 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(
|
height16,
|
||||||
height: 16,
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child:
|
||||||
|
Text(tr('dontShowTrackOnlyWarnings'))),
|
||||||
|
Switch(
|
||||||
|
value:
|
||||||
|
settingsProvider.hideTrackOnlyWarning,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.hideTrackOnlyWarning =
|
||||||
|
value;
|
||||||
|
})
|
||||||
|
],
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
Text(
|
Row(
|
||||||
tr('updates'),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: TextStyle(
|
children: [
|
||||||
color: Theme.of(context).colorScheme.primary),
|
Flexible(
|
||||||
),
|
child:
|
||||||
intervalDropdown,
|
Text(tr('dontShowAPKOriginWarnings'))),
|
||||||
const Divider(
|
Switch(
|
||||||
height: 48,
|
value:
|
||||||
),
|
settingsProvider.hideAPKOriginWarning,
|
||||||
Text(
|
onChanged: (value) {
|
||||||
tr('sourceSpecific'),
|
settingsProvider.hideAPKOriginWarning =
|
||||||
style: TextStyle(
|
value;
|
||||||
color: Theme.of(context).colorScheme.primary),
|
})
|
||||||
),
|
],
|
||||||
...sourceSpecificFields,
|
|
||||||
const Divider(
|
|
||||||
height: 48,
|
|
||||||
),
|
),
|
||||||
|
height32,
|
||||||
Text(
|
Text(
|
||||||
tr('categories'),
|
tr('categories'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
@ -444,6 +515,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
storedValues = settingsProvider.categories.map((key, value) => MapEntry(
|
storedValues = settingsProvider.categories.map((key, value) => MapEntry(
|
||||||
key,
|
key,
|
||||||
MapEntry(value,
|
MapEntry(value,
|
||||||
@ -467,8 +539,9 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
|
|||||||
if (!isBuilding) {
|
if (!isBuilding) {
|
||||||
storedValues =
|
storedValues =
|
||||||
values['categories'] as Map<String, MapEntry<int, bool>>;
|
values['categories'] as Map<String, MapEntry<int, bool>>;
|
||||||
settingsProvider.categories =
|
settingsProvider.setCategories(
|
||||||
storedValues.map((key, value) => MapEntry(key, value.key));
|
storedValues.map((key, value) => MapEntry(key, value.key)),
|
||||||
|
appsProvider: appsProvider);
|
||||||
if (widget.onSelected != null) {
|
if (widget.onSelected != null) {
|
||||||
widget.onSelected!(storedValues.keys
|
widget.onSelected!(storedValues.keys
|
||||||
.where((k) => storedValues[k]!.value)
|
.where((k) => storedValues[k]!.value)
|
||||||
|
@ -6,11 +6,11 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:android_intent_plus/flag.dart';
|
import 'package:android_intent_plus/flag.dart';
|
||||||
|
import 'package:android_package_installer/android_package_installer.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
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:install_plugin_v2/install_plugin_v2.dart';
|
|
||||||
import 'package:installed_apps/app_info.dart';
|
import 'package:installed_apps/app_info.dart';
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
@ -27,6 +27,7 @@ import 'package:flutter_fgbg/flutter_fgbg.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
import 'package:flutter_archive/flutter_archive.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -34,6 +35,10 @@ class AppInMemory {
|
|||||||
AppInfo? installedInfo;
|
AppInfo? installedInfo;
|
||||||
|
|
||||||
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
|
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
|
||||||
|
AppInMemory deepCopy() =>
|
||||||
|
AppInMemory(app.deepCopy(), downloadProgress, installedInfo);
|
||||||
|
|
||||||
|
String get name => app.overrideName ?? installedInfo?.name ?? app.finalName;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadedApk {
|
class DownloadedApk {
|
||||||
@ -42,6 +47,13 @@ class DownloadedApk {
|
|||||||
DownloadedApk(this.appId, this.file);
|
DownloadedApk(this.appId, this.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DownloadedXApkDir {
|
||||||
|
String appId;
|
||||||
|
File file;
|
||||||
|
Directory extracted;
|
||||||
|
DownloadedXApkDir(this.appId, this.file, this.extracted);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> generateStandardVersionRegExStrings() {
|
List<String> generateStandardVersionRegExStrings() {
|
||||||
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
// TODO: Look into RegEx for non-Latin characters / non-Arabic numerals
|
||||||
var basics = [
|
var basics = [
|
||||||
@ -96,39 +108,65 @@ class AppsProvider with ChangeNotifier {
|
|||||||
bool isForeground = true;
|
bool isForeground = true;
|
||||||
late Stream<FGBGType>? foregroundStream;
|
late Stream<FGBGType>? foregroundStream;
|
||||||
late StreamSubscription<FGBGType>? foregroundSubscription;
|
late StreamSubscription<FGBGType>? foregroundSubscription;
|
||||||
|
late Directory APKDir;
|
||||||
|
|
||||||
|
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
|
||||||
|
|
||||||
AppsProvider() {
|
AppsProvider() {
|
||||||
// Subscribe to changes in the app foreground status
|
// Subscribe to changes in the app foreground status
|
||||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||||
isForeground = event == FGBGType.foreground;
|
isForeground = event == FGBGType.foreground;
|
||||||
if (isForeground) await loadApps();
|
if (isForeground) await refreshInstallStatuses();
|
||||||
});
|
});
|
||||||
() async {
|
() async {
|
||||||
|
var cacheDirs = await getExternalCacheDirectories();
|
||||||
|
if (cacheDirs?.isNotEmpty ?? false) {
|
||||||
|
APKDir = cacheDirs!.first;
|
||||||
|
} else {
|
||||||
|
APKDir =
|
||||||
|
Directory('${(await getExternalStorageDirectory())!.path}/apks');
|
||||||
|
if (!APKDir.existsSync()) {
|
||||||
|
APKDir.createSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
||||||
await loadApps();
|
await loadApps();
|
||||||
// Delete existing APKs
|
// Delete any partial APKs
|
||||||
(await getExternalStorageDirectory())
|
var cutoff = DateTime.now().subtract(const Duration(days: 7));
|
||||||
?.listSync()
|
APKDir.listSync()
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
element.path.endsWith('.apk') ||
|
element.path.endsWith('.part') ||
|
||||||
element.path.endsWith('.apk.part'))
|
element.statSync().modified.isBefore(cutoff))
|
||||||
.forEach((apk) {
|
.forEach((partialApk) {
|
||||||
apk.delete();
|
partialApk.delete(recursive: true);
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(String url, String fileName, Function? onProgress,
|
Future<File> downloadFile(
|
||||||
{bool useExisting = true}) async {
|
String url, String fileNameNoExt, Function? onProgress,
|
||||||
var destDir = (await getExternalStorageDirectory())!.path;
|
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||||
StreamedResponse response =
|
var destDir = APKDir.path;
|
||||||
await Client().send(Request('GET', Uri.parse(url)));
|
var req = Request('GET', Uri.parse(url));
|
||||||
File downloadedFile = File('$destDir/$fileName');
|
if (headers != null) {
|
||||||
|
req.headers.addAll(headers);
|
||||||
|
}
|
||||||
|
var client = Client();
|
||||||
|
StreamedResponse response = await client.send(req);
|
||||||
|
String ext =
|
||||||
|
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
||||||
|
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||||
|
ext = ext.substring(0, ext.length - 1);
|
||||||
|
}
|
||||||
|
if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
|
||||||
|
ext = 'apk';
|
||||||
|
}
|
||||||
|
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
|
||||||
if (!(downloadedFile.existsSync() && useExisting)) {
|
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||||
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||||
if (tempDownloadedFile.existsSync()) {
|
if (tempDownloadedFile.existsSync()) {
|
||||||
tempDownloadedFile.deleteSync();
|
tempDownloadedFile.deleteSync(recursive: true);
|
||||||
}
|
}
|
||||||
var length = response.contentLength;
|
var length = response.contentLength;
|
||||||
var received = 0;
|
var received = 0;
|
||||||
@ -148,70 +186,108 @@ class AppsProvider with ChangeNotifier {
|
|||||||
onProgress(progress);
|
onProgress(progress);
|
||||||
}
|
}
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
tempDownloadedFile.deleteSync();
|
tempDownloadedFile.deleteSync(recursive: true);
|
||||||
throw response.reasonPhrase ?? tr('unexpectedError');
|
throw response.reasonPhrase ?? tr('unexpectedError');
|
||||||
}
|
}
|
||||||
tempDownloadedFile.renameSync(downloadedFile.path);
|
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||||
|
} else {
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
return downloadedFile;
|
return downloadedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloadedApk> downloadApp(App app, BuildContext? context) async {
|
Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo,
|
||||||
|
File downloadedFile, String downloadUrl) async {
|
||||||
|
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
||||||
|
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||||
|
if (app.id != newInfo.packageName) {
|
||||||
|
var isTempId = SourceProvider().isTempId(app);
|
||||||
|
if (apps[app.id] != null && !isTempId && !app.allowIdChange) {
|
||||||
|
throw IDChangedError(newInfo.packageName);
|
||||||
|
}
|
||||||
|
var idChangeWasAllowed = app.allowIdChange;
|
||||||
|
app.allowIdChange = false;
|
||||||
|
var originalAppId = app.id;
|
||||||
|
app.id = newInfo.packageName;
|
||||||
|
downloadedFile = downloadedFile.renameSync(
|
||||||
|
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
|
||||||
|
if (apps[originalAppId] != null) {
|
||||||
|
await removeApps([originalAppId]);
|
||||||
|
await saveApps([app], onlyIfExists: !isTempId && !idChangeWasAllowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downloadedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||||
NotificationsProvider? notificationsProvider =
|
NotificationsProvider? notificationsProvider =
|
||||||
context?.read<NotificationsProvider>();
|
context?.read<NotificationsProvider>();
|
||||||
var notifId = DownloadNotification(app.name, 0).id;
|
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
apps[app.id]!.downloadProgress = 0;
|
apps[app.id]!.downloadProgress = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var fileName =
|
AppSource source = SourceProvider()
|
||||||
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk';
|
.getSource(app.url, overrideSource: app.overrideSource);
|
||||||
String downloadUrl = await SourceProvider()
|
String downloadUrl = await source.apkUrlPrefetchModifier(
|
||||||
.getSource(app.url)
|
app.apkUrls[app.preferredApkIndex].value, app.url);
|
||||||
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex]);
|
var notif = DownloadNotification(app.finalName, 100);
|
||||||
var notif = DownloadNotification(app.name, 100);
|
|
||||||
notificationsProvider?.cancel(notif.id);
|
notificationsProvider?.cancel(notif.id);
|
||||||
int? prevProg;
|
int? prevProg;
|
||||||
File downloadedFile =
|
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
||||||
await downloadFile(downloadUrl, fileName, (double? progress) {
|
var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt,
|
||||||
|
headers: source.requestHeaders, (double? progress) {
|
||||||
int? prog = progress?.ceil();
|
int? prog = progress?.ceil();
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
apps[app.id]!.downloadProgress = progress;
|
apps[app.id]!.downloadProgress = progress;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
notif = DownloadNotification(app.name, prog ?? 100);
|
notif = DownloadNotification(app.finalName, prog ?? 100);
|
||||||
if (prog != null && prevProg != prog) {
|
if (prog != null && prevProg != prog) {
|
||||||
notificationsProvider?.notify(notif);
|
notificationsProvider?.notify(notif);
|
||||||
}
|
}
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
});
|
||||||
// Delete older versions of the APK if any
|
// Set to 90 for remaining steps, will make null in 'finally'
|
||||||
|
if (apps[app.id] != null) {
|
||||||
|
apps[app.id]!.downloadProgress = -1;
|
||||||
|
notifyListeners();
|
||||||
|
notif = DownloadNotification(app.finalName, -1);
|
||||||
|
notificationsProvider?.notify(notif);
|
||||||
|
}
|
||||||
|
PackageArchiveInfo? newInfo;
|
||||||
|
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
|
||||||
|
Directory? xapkDir;
|
||||||
|
if (isAPK) {
|
||||||
|
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||||
|
} else {
|
||||||
|
// Assume XAPK
|
||||||
|
String xapkDirPath = '${downloadedFile.path}-dir';
|
||||||
|
await unzipFile(downloadedFile.path, '${downloadedFile.path}-dir');
|
||||||
|
xapkDir = Directory(xapkDirPath);
|
||||||
|
var apks = xapkDir
|
||||||
|
.listSync()
|
||||||
|
.where((e) => e.path.toLowerCase().endsWith('.apk'))
|
||||||
|
.toList();
|
||||||
|
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
|
||||||
|
}
|
||||||
|
downloadedFile =
|
||||||
|
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
|
||||||
|
// Delete older versions of the file if any
|
||||||
for (var file in downloadedFile.parent.listSync()) {
|
for (var file in downloadedFile.parent.listSync()) {
|
||||||
var fn = file.path.split('/').last;
|
var fn = file.path.split('/').last;
|
||||||
if (fn.startsWith('${app.id}-') &&
|
if (fn.startsWith('${app.id}-') &&
|
||||||
fn.endsWith('.apk') &&
|
FileSystemEntity.isFileSync(file.path) &&
|
||||||
fn != fileName) {
|
file.path != downloadedFile.path) {
|
||||||
file.delete();
|
file.delete(recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
if (isAPK) {
|
||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
} else {
|
||||||
if (app.id != newInfo.packageName) {
|
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
|
||||||
if (apps[app.id] != null && !SourceProvider().isTempId(app)) {
|
|
||||||
throw IDChangedError();
|
|
||||||
}
|
|
||||||
var originalAppId = app.id;
|
|
||||||
app.id = newInfo.packageName;
|
|
||||||
downloadedFile = downloadedFile.renameSync(
|
|
||||||
'${downloadedFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
|
||||||
if (apps[originalAppId] != null) {
|
|
||||||
await removeApps([originalAppId]);
|
|
||||||
await saveApps([app]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
|
||||||
} finally {
|
} finally {
|
||||||
notificationsProvider?.cancel(notifId);
|
notificationsProvider?.cancel(notifId);
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
@ -258,11 +334,33 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
Future<void> unzipFile(String filePath, String destinationPath) async {
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
await ZipFile.extractToDirectory(zipFile: File(filePath), destinationDir: Directory(destinationPath));
|
||||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
}
|
||||||
// But even then, we don't know if it actually succeeded
|
|
||||||
Future<void> installApk(DownloadedApk file) async {
|
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||||
|
{bool silent = false}) async {
|
||||||
|
try {
|
||||||
|
var somethingInstalled = false;
|
||||||
|
for (var file in dir.extracted.listSync(recursive: true, followLinks: false).whereType<File>()) {
|
||||||
|
if (file.path.toLowerCase().endsWith('.apk')) {
|
||||||
|
somethingInstalled = somethingInstalled ||
|
||||||
|
await installApk(DownloadedApk(dir.appId, file), silent: silent);
|
||||||
|
}
|
||||||
|
else if (file.path.toLowerCase().endsWith('.obb')) {
|
||||||
|
await moveObbFile(file, dir.appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (somethingInstalled) {
|
||||||
|
dir.file.delete(recursive: true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dir.extracted.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||||
|
// TODO: Use 'silent' when/if ever possible
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||||
AppInfo? appInfo;
|
AppInfo? appInfo;
|
||||||
try {
|
try {
|
||||||
@ -275,16 +373,34 @@ class AppsProvider with ChangeNotifier {
|
|||||||
!(await canDowngradeApps())) {
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
await InstallPlugin.installApk(file.file.path, obtainiumId);
|
int? code =
|
||||||
if (file.appId == obtainiumId) {
|
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||||
// Obtainium prompt should be lowest
|
bool installed = false;
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
if (code != null && code != 0 && code != 3) {
|
||||||
|
throw InstallError(code);
|
||||||
|
} else if (code == 0) {
|
||||||
|
installed = true;
|
||||||
|
apps[file.appId]!.app.installedVersion =
|
||||||
|
apps[file.appId]!.app.latestVersion;
|
||||||
|
file.file.delete(recursive: true);
|
||||||
}
|
}
|
||||||
apps[file.appId]!.app.installedVersion =
|
await saveApps([apps[file.appId]!.app]);
|
||||||
apps[file.appId]!.app.latestVersion;
|
return installed;
|
||||||
// Don't correct install status as installation may not be done yet
|
}
|
||||||
await saveApps([apps[file.appId]!.app],
|
|
||||||
attemptToCorrectInstallStatus: false);
|
Future<void> moveObbFile(File file, String appId) async {
|
||||||
|
if(!file.path.toLowerCase().endsWith('.obb')) return;
|
||||||
|
|
||||||
|
// TODO: Does not support Android 11+
|
||||||
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
||||||
|
await Permission.storage.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
String obbDirPath = "/storage/emulated/0/Android/obb/$appId";
|
||||||
|
Directory(obbDirPath).createSync(recursive: true);
|
||||||
|
|
||||||
|
String obbFileName = file.path.split("/").last;
|
||||||
|
await file.copy("$obbDirPath/$obbFileName");
|
||||||
}
|
}
|
||||||
|
|
||||||
void uninstallApp(String appId) async {
|
void uninstallApp(String appId) async {
|
||||||
@ -296,9 +412,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
await intent.launch();
|
await intent.launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> confirmApkUrl(App app, BuildContext? context) async {
|
Future<MapEntry<String, String>?> confirmApkUrl(
|
||||||
|
App app, BuildContext? context) async {
|
||||||
// If the App has more than one APK, the user should pick one (if context provided)
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
MapEntry<String, String>? apkUrl =
|
||||||
|
app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||||
// get device supported architecture
|
// get device supported architecture
|
||||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
|
||||||
@ -321,16 +439,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
getHost(apkUrl) != getHost(app.url) &&
|
getHost(apkUrl.value) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (await showDialog(
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
context: context,
|
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||||
builder: (BuildContext ctx) {
|
// ignore: use_build_context_synchronously
|
||||||
return APKOriginWarningDialog(
|
await showDialog(
|
||||||
sourceUrl: app.url, apkUrl: apkUrl!);
|
context: context,
|
||||||
}) !=
|
builder: (BuildContext ctx) {
|
||||||
true) {
|
return APKOriginWarningDialog(
|
||||||
|
sourceUrl: app.url, apkUrl: apkUrl!.value);
|
||||||
|
}) !=
|
||||||
|
true) {
|
||||||
apkUrl = null;
|
apkUrl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,7 +464,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
|
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
|
||||||
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
||||||
Future<List<String>> downloadAndInstallLatestApps(
|
Future<List<String>> downloadAndInstallLatestApps(
|
||||||
List<String> appIds, BuildContext? context) async {
|
List<String> appIds, BuildContext? context,
|
||||||
|
{SettingsProvider? settingsProvider}) async {
|
||||||
List<String> appsToInstall = [];
|
List<String> appsToInstall = [];
|
||||||
List<String> trackOnlyAppsToUpdate = [];
|
List<String> trackOnlyAppsToUpdate = [];
|
||||||
// For all specified Apps, filter out those for which:
|
// For all specified Apps, filter out those for which:
|
||||||
@ -353,14 +475,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apps[id] == null) {
|
if (apps[id] == null) {
|
||||||
throw ObtainiumError(tr('appNotFound'));
|
throw ObtainiumError(tr('appNotFound'));
|
||||||
}
|
}
|
||||||
String? apkUrl;
|
MapEntry<String, String>? apkUrl;
|
||||||
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
|
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
|
||||||
if (!trackOnly) {
|
if (!trackOnly) {
|
||||||
apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
||||||
}
|
}
|
||||||
if (apkUrl != null) {
|
if (apkUrl != null) {
|
||||||
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
int urlInd = apps[id]!
|
||||||
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
.app
|
||||||
|
.apkUrls
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toList()
|
||||||
|
.indexOf(apkUrl.value);
|
||||||
|
if (urlInd >= 0 && urlInd != apps[id]!.app.preferredApkIndex) {
|
||||||
apps[id]!.app.preferredApkIndex = urlInd;
|
apps[id]!.app.preferredApkIndex = urlInd;
|
||||||
await saveApps([apps[id]!.app]);
|
await saveApps([apps[id]!.app]);
|
||||||
}
|
}
|
||||||
@ -378,71 +505,62 @@ class AppsProvider with ChangeNotifier {
|
|||||||
a.installedVersion = a.latestVersion;
|
a.installedVersion = a.latestVersion;
|
||||||
return a;
|
return a;
|
||||||
}).toList());
|
}).toList());
|
||||||
// Download APKs for all Apps to be installed
|
|
||||||
|
// Prepare to download+install Apps
|
||||||
MultiAppMultiError errors = MultiAppMultiError();
|
MultiAppMultiError errors = MultiAppMultiError();
|
||||||
List<DownloadedApk?> downloadedFiles =
|
List<String> installedIds = [];
|
||||||
await Future.wait(appsToInstall.map((id) async {
|
|
||||||
|
// Move Obtainium to the end of the line (let all other apps update first)
|
||||||
|
String? temp;
|
||||||
|
appsToInstall.removeWhere((element) {
|
||||||
|
bool res = element == obtainiumId || element == obtainiumTempId;
|
||||||
|
if (res) {
|
||||||
|
temp = element;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
if (temp != null) {
|
||||||
|
appsToInstall = [...appsToInstall, temp!];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var id in appsToInstall) {
|
||||||
try {
|
try {
|
||||||
return await downloadApp(apps[id]!.app, context);
|
// ignore: use_build_context_synchronously
|
||||||
|
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||||
|
DownloadedApk? downloadedFile;
|
||||||
|
DownloadedXApkDir? downloadedDir;
|
||||||
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
|
downloadedFile = downloadedArtifact;
|
||||||
|
} else {
|
||||||
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
|
}
|
||||||
|
bool willBeSilent = await canInstallSilently(
|
||||||
|
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
|
||||||
|
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||||
|
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||||
|
true)) {
|
||||||
|
throw ObtainiumError(tr('cancelled'));
|
||||||
|
}
|
||||||
|
if (!willBeSilent && context != null) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
await waitForUserToReturnToForeground(context);
|
||||||
|
}
|
||||||
|
apps[id]?.downloadProgress = -1;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
if (downloadedFile != null) {
|
||||||
|
await installApk(downloadedFile, silent: willBeSilent);
|
||||||
|
} else {
|
||||||
|
await installXApkDir(downloadedDir!, silent: willBeSilent);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
apps[id]?.downloadProgress = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
installedIds.add(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add(id, e.toString());
|
errors.add(id, e.toString());
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}));
|
|
||||||
downloadedFiles =
|
|
||||||
downloadedFiles.where((element) => element != null).toList();
|
|
||||||
// Separate the Apps to install into silent and regular lists
|
|
||||||
List<DownloadedApk> silentUpdates = [];
|
|
||||||
List<DownloadedApk> regularInstalls = [];
|
|
||||||
for (var f in downloadedFiles) {
|
|
||||||
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
|
|
||||||
if (willBeSilent) {
|
|
||||||
silentUpdates.add(f);
|
|
||||||
} else {
|
|
||||||
regularInstalls.add(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move everything to the regular install list (since silent updates don't currently work)
|
|
||||||
// TODO: Remove this when silent updates work
|
|
||||||
regularInstalls.addAll(silentUpdates);
|
|
||||||
|
|
||||||
// If Obtainium is being installed, it should be the last one
|
|
||||||
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
|
|
||||||
DownloadedApk? temp;
|
|
||||||
items.removeWhere((element) {
|
|
||||||
bool res =
|
|
||||||
element.appId == obtainiumId || element.appId == obtainiumTempId;
|
|
||||||
if (res) {
|
|
||||||
temp = element;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
if (temp != null) {
|
|
||||||
items = [temp!, ...items];
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
silentUpdates = moveObtainiumToStart(silentUpdates);
|
|
||||||
regularInstalls = moveObtainiumToStart(regularInstalls);
|
|
||||||
|
|
||||||
// // Install silent updates (uncomment when it works - TODO)
|
|
||||||
// for (var u in silentUpdates) {
|
|
||||||
// await installApk(u, silent: true); // Would need to add silent option
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Do regular installs
|
|
||||||
if (regularInstalls.isNotEmpty && context != null) {
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
await waitForUserToReturnToForeground(context);
|
|
||||||
for (var i in regularInstalls) {
|
|
||||||
try {
|
|
||||||
await installApk(i);
|
|
||||||
} catch (e) {
|
|
||||||
errors.add(i.appId, e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.content.isNotEmpty) {
|
if (errors.content.isNotEmpty) {
|
||||||
@ -451,7 +569,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
NotificationsProvider().cancel(UpdateNotification([]).id);
|
NotificationsProvider().cancel(UpdateNotification([]).id);
|
||||||
|
|
||||||
return downloadedFiles.map((e) => e!.appId).toList();
|
return installedIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
@ -603,41 +721,30 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
loadingApps = true;
|
loadingApps = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
List<App> newApps = (await getAppsDir())
|
|
||||||
.listSync()
|
|
||||||
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
|
||||||
.map((e) {
|
|
||||||
try {
|
|
||||||
return App.fromJson(jsonDecode(File(e.path).readAsStringSync()));
|
|
||||||
} catch (err) {
|
|
||||||
if (err is FormatException) {
|
|
||||||
logs.add('Corrupt JSON when loading App (will be ignored): $e');
|
|
||||||
e.renameSync('${e.path}.corrupt');
|
|
||||||
return App(
|
|
||||||
'', '', '', '', '', '', [], 0, {}, DateTime.now(), false);
|
|
||||||
} else {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.where((element) => element.id.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
var idsToDelete = apps.values
|
|
||||||
.map((e) => e.app.id)
|
|
||||||
.toSet()
|
|
||||||
.difference(newApps.map((e) => e.id).toSet());
|
|
||||||
for (var id in idsToDelete) {
|
|
||||||
apps.remove(id);
|
|
||||||
}
|
|
||||||
var sp = SourceProvider();
|
var sp = SourceProvider();
|
||||||
List<List<String>> errors = [];
|
List<List<String>> errors = [];
|
||||||
for (int i = 0; i < newApps.length; i++) {
|
List<FileSystemEntity> newApps = (await getAppsDir())
|
||||||
var info = await getInstalledInfo(newApps[i].id);
|
.listSync()
|
||||||
|
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
||||||
|
.toList();
|
||||||
|
for (var e in newApps) {
|
||||||
try {
|
try {
|
||||||
sp.getSource(newApps[i].url);
|
var app = App.fromJson(jsonDecode(File(e.path).readAsStringSync()));
|
||||||
apps[newApps[i].id] = AppInMemory(newApps[i], null, info);
|
try {
|
||||||
} catch (e) {
|
var info = await getInstalledInfo(app.id);
|
||||||
errors.add([newApps[i].id, newApps[i].name, e.toString()]);
|
sp.getSource(app.url, overrideSource: app.overrideSource);
|
||||||
|
apps[app.id] = AppInMemory(app, null, info);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
errors.add([app.id, app.finalName, e.toString()]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err is FormatException) {
|
||||||
|
logs.add('Corrupt JSON when loading App (will be ignored): $e');
|
||||||
|
e.renameSync('${e.path}.corrupt');
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errors.isNotEmpty) {
|
if (errors.isNotEmpty) {
|
||||||
@ -647,6 +754,10 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
loadingApps = false;
|
loadingApps = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
refreshInstallStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshInstallStatuses() async {
|
||||||
if (await doesInstalledAppsPluginWork()) {
|
if (await doesInstalledAppsPluginWork()) {
|
||||||
List<App> modifiedApps = [];
|
List<App> modifiedApps = [];
|
||||||
for (var app in apps.values) {
|
for (var app in apps.values) {
|
||||||
@ -667,14 +778,17 @@ class AppsProvider with ChangeNotifier {
|
|||||||
bool onlyIfExists = true}) async {
|
bool onlyIfExists = true}) async {
|
||||||
attemptToCorrectInstallStatus =
|
attemptToCorrectInstallStatus =
|
||||||
attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
|
attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
|
||||||
for (var app in apps) {
|
for (var a in apps) {
|
||||||
|
var app = a.deepCopy();
|
||||||
AppInfo? info = await getInstalledInfo(app.id);
|
AppInfo? info = await getInstalledInfo(app.id);
|
||||||
app.name = info?.name ?? app.name;
|
app.name = info?.name ?? app.name;
|
||||||
if (attemptToCorrectInstallStatus) {
|
if (attemptToCorrectInstallStatus) {
|
||||||
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
|
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
|
||||||
}
|
}
|
||||||
File('${(await getAppsDir()).path}/${app.id}.json')
|
if (!onlyIfExists || this.apps.containsKey(app.id)) {
|
||||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
File('${(await getAppsDir()).path}/${app.id}.json')
|
||||||
|
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this.apps.update(
|
this.apps.update(
|
||||||
app.id, (value) => AppInMemory(app, value.downloadProgress, info),
|
app.id, (value) => AppInMemory(app, value.downloadProgress, info),
|
||||||
@ -689,11 +803,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeApps(List<String> appIds) async {
|
Future<void> removeApps(List<String> appIds) async {
|
||||||
|
var apkFiles = APKDir.listSync();
|
||||||
for (var appId in appIds) {
|
for (var appId in appIds) {
|
||||||
File file = File('${(await getAppsDir()).path}/$appId.json');
|
File file = File('${(await getAppsDir()).path}/$appId.json');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
file.deleteSync();
|
file.deleteSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
apkFiles
|
||||||
|
.where(
|
||||||
|
(element) => element.path.split('/').last.startsWith('$appId-'))
|
||||||
|
.forEach((element) {
|
||||||
|
element.delete(recursive: true);
|
||||||
|
});
|
||||||
if (apps.containsKey(appId)) {
|
if (apps.containsKey(appId)) {
|
||||||
apps.remove(appId);
|
apps.remove(appId);
|
||||||
}
|
}
|
||||||
@ -757,11 +878,24 @@ class AppsProvider with ChangeNotifier {
|
|||||||
await intent.launch();
|
await intent.launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMissingCategories(SettingsProvider settingsProvider) {
|
||||||
|
var cats = settingsProvider.categories;
|
||||||
|
apps.forEach((key, value) {
|
||||||
|
for (var c in value.app.categories) {
|
||||||
|
if (!cats.containsKey(c)) {
|
||||||
|
cats[c] = generateRandomLightColor().value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
settingsProvider.setCategories(cats, appsProvider: this);
|
||||||
|
}
|
||||||
|
|
||||||
Future<App?> checkUpdate(String appId) async {
|
Future<App?> checkUpdate(String appId) async {
|
||||||
App? currentApp = apps[appId]!.app;
|
App? currentApp = apps[appId]!.app;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
App newApp = await sourceProvider.getApp(
|
App newApp = await sourceProvider.getApp(
|
||||||
sourceProvider.getSource(currentApp.url),
|
sourceProvider.getSource(currentApp.url,
|
||||||
|
overrideSource: currentApp.overrideSource),
|
||||||
currentApp.url,
|
currentApp.url,
|
||||||
currentApp.additionalSettings,
|
currentApp.additionalSettings,
|
||||||
currentApp: currentApp);
|
currentApp: currentApp);
|
||||||
@ -836,12 +970,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> exportApps() async {
|
Future<String> exportApps() async {
|
||||||
Directory? exportDir = Directory('/storage/emulated/0/Download');
|
|
||||||
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
|
|
||||||
if (!exportDir.existsSync()) {
|
|
||||||
exportDir = await getExternalStorageDirectory();
|
|
||||||
path = exportDir!.path;
|
|
||||||
}
|
|
||||||
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
||||||
if (await Permission.storage.isDenied) {
|
if (await Permission.storage.isDenied) {
|
||||||
await Permission.storage.request();
|
await Permission.storage.request();
|
||||||
@ -850,6 +978,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
throw ObtainiumError(tr('storagePermissionDenied'));
|
throw ObtainiumError(tr('storagePermissionDenied'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Directory? exportDir = Directory('/storage/emulated/0/Download');
|
||||||
|
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
|
||||||
|
var downloadsAccessible = false;
|
||||||
|
try {
|
||||||
|
downloadsAccessible = exportDir.existsSync();
|
||||||
|
} catch (e) {
|
||||||
|
logs.add('Error accessing Downloads (will use fallback): $e');
|
||||||
|
}
|
||||||
|
if (!downloadsAccessible) {
|
||||||
|
exportDir = await getExternalStorageDirectory();
|
||||||
|
path = exportDir!.path;
|
||||||
|
}
|
||||||
File export = File(
|
File export = File(
|
||||||
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
||||||
export.writeAsStringSync(
|
export.writeAsStringSync(
|
||||||
@ -882,7 +1022,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<List<List<String>>> addAppsByURL(List<String> urls) async {
|
Future<List<List<String>>> addAppsByURL(List<String> urls) async {
|
||||||
List<dynamic> results = await SourceProvider().getAppsByURLNaive(urls,
|
List<dynamic> results = await SourceProvider().getAppsByURLNaive(urls,
|
||||||
ignoreUrls: apps.values.map((e) => e.app.url).toList());
|
alreadyAddedUrls: apps.values.map((e) => e.app.url).toList());
|
||||||
List<App> pps = results[0];
|
List<App> pps = results[0];
|
||||||
Map<String, dynamic> errorsMap = results[1];
|
Map<String, dynamic> errorsMap = results[1];
|
||||||
for (var app in pps) {
|
for (var app in pps) {
|
||||||
@ -902,7 +1042,7 @@ class APKPicker extends StatefulWidget {
|
|||||||
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
||||||
|
|
||||||
final App app;
|
final App app;
|
||||||
final String? initVal;
|
final MapEntry<String, String>? initVal;
|
||||||
final List<String>? archs;
|
final List<String>? archs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -910,7 +1050,7 @@ class APKPicker extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _APKPickerState extends State<APKPicker> {
|
class _APKPickerState extends State<APKPicker> {
|
||||||
String? apkUrl;
|
MapEntry<String, String>? apkUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -919,19 +1059,17 @@ class _APKPickerState extends State<APKPicker> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(tr('pickAnAPK')),
|
title: Text(tr('pickAnAPK')),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
Text(tr('appHasMoreThanOnePackage', args: [widget.app.name])),
|
Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
...widget.app.apkUrls.map(
|
...widget.app.apkUrls.map(
|
||||||
(u) => RadioListTile<String>(
|
(u) => RadioListTile<String>(
|
||||||
title: Text(Uri.parse(u)
|
title: Text(u.key),
|
||||||
.pathSegments
|
value: u.value,
|
||||||
.where((element) => element.isNotEmpty)
|
groupValue: apkUrl!.value,
|
||||||
.last),
|
|
||||||
value: u,
|
|
||||||
groupValue: apkUrl,
|
|
||||||
onChanged: (String? val) {
|
onChanged: (String? val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
apkUrl = val;
|
apkUrl =
|
||||||
|
widget.app.apkUrls.where((e) => e.value == val).first;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -34,9 +34,9 @@ class UpdateNotification extends ObtainiumNotification {
|
|||||||
message = updates.isEmpty
|
message = updates.isEmpty
|
||||||
? tr('noNewUpdates')
|
? tr('noNewUpdates')
|
||||||
: updates.length == 1
|
: updates.length == 1
|
||||||
? tr('xHasAnUpdate', args: [updates[0].name])
|
? tr('xHasAnUpdate', args: [updates[0].finalName])
|
||||||
: plural('xAndNMoreUpdatesAvailable', updates.length - 1,
|
: plural('xAndNMoreUpdatesAvailable', updates.length - 1,
|
||||||
args: [updates[0].name, (updates.length - 1).toString()]);
|
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +46,9 @@ class SilentUpdateNotification extends ObtainiumNotification {
|
|||||||
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
|
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
|
||||||
message = updates.length == 1
|
message = updates.length == 1
|
||||||
? tr('xWasUpdatedToY',
|
? tr('xWasUpdatedToY',
|
||||||
args: [updates[0].name, updates[0].latestVersion])
|
args: [updates[0].finalName, updates[0].latestVersion])
|
||||||
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
|
: plural('xAndNMoreUpdatesInstalled', updates.length - 1,
|
||||||
args: [updates[0].name, (updates.length - 1).toString()]);
|
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +167,8 @@ class NotificationsProvider {
|
|||||||
progress: progPercent ?? 0,
|
progress: progPercent ?? 0,
|
||||||
maxProgress: 100,
|
maxProgress: 100,
|
||||||
showProgress: progPercent != null,
|
showProgress: progPercent != null,
|
||||||
onlyAlertOnce: onlyAlertOnce)));
|
onlyAlertOnce: onlyAlertOnce,
|
||||||
|
indeterminate: progPercent != null && progPercent < 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notify(ObtainiumNotification notif,
|
Future<void> notify(ObtainiumNotification notif,
|
||||||
|
@ -7,6 +7,8 @@ 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/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.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';
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
|||||||
|
|
||||||
class SettingsProvider with ChangeNotifier {
|
class SettingsProvider with ChangeNotifier {
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
|
bool justStarted = true;
|
||||||
|
|
||||||
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
||||||
|
|
||||||
@ -62,6 +65,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get useBlackTheme {
|
||||||
|
return prefs?.getBool('useBlackTheme') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set useBlackTheme(bool useBlackTheme) {
|
||||||
|
prefs?.setBool('useBlackTheme', useBlackTheme);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
int get updateInterval {
|
int get updateInterval {
|
||||||
var min = prefs?.getInt('updateInterval') ?? 360;
|
var min = prefs?.getInt('updateInterval') ?? 360;
|
||||||
if (!updateIntervals.contains(min)) {
|
if (!updateIntervals.contains(min)) {
|
||||||
@ -81,6 +93,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get checkOnStart {
|
||||||
|
return prefs?.getBool('checkOnStart') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set checkOnStart(bool checkOnStart) {
|
||||||
|
prefs?.setBool('checkOnStart', checkOnStart);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
SortColumnSettings get sortColumn {
|
SortColumnSettings get sortColumn {
|
||||||
return SortColumnSettings.values[
|
return SortColumnSettings.values[
|
||||||
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
prefs?.getInt('sortColumn') ?? SortColumnSettings.nameAuthor.index];
|
||||||
@ -109,16 +130,28 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getInstallPermission() async {
|
bool checkJustStarted() {
|
||||||
|
if (justStarted) {
|
||||||
|
justStarted = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getInstallPermission({bool enforce = false}) async {
|
||||||
while (!(await Permission.requestInstallPackages.isGranted)) {
|
while (!(await Permission.requestInstallPackages.isGranted)) {
|
||||||
// Explicit request as InstallPlugin request sometimes bugged
|
// Explicit request as InstallPlugin request sometimes bugged
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
msg: tr('pleaseAllowInstallPerm'), toastLength: Toast.LENGTH_LONG);
|
||||||
if ((await Permission.requestInstallPackages.request()) ==
|
if ((await Permission.requestInstallPackages.request()) ==
|
||||||
PermissionStatus.granted) {
|
PermissionStatus.granted) {
|
||||||
break;
|
return true;
|
||||||
|
}
|
||||||
|
if (!enforce) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get showAppWebpage {
|
bool get showAppWebpage {
|
||||||
@ -139,6 +172,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get buryNonInstalled {
|
||||||
|
return prefs?.getBool('buryNonInstalled') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set buryNonInstalled(bool show) {
|
||||||
|
prefs?.setBool('buryNonInstalled', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
bool get groupByCategory {
|
bool get groupByCategory {
|
||||||
return prefs?.getBool('groupByCategory') ?? false;
|
return prefs?.getBool('groupByCategory') ?? false;
|
||||||
}
|
}
|
||||||
@ -148,6 +190,24 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hideTrackOnlyWarning {
|
||||||
|
return prefs?.getBool('hideTrackOnlyWarning') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set hideTrackOnlyWarning(bool show) {
|
||||||
|
prefs?.setBool('hideTrackOnlyWarning', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hideAPKOriginWarning {
|
||||||
|
return prefs?.getBool('hideAPKOriginWarning') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set hideAPKOriginWarning(bool show) {
|
||||||
|
prefs?.setBool('hideAPKOriginWarning', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
String? getSettingString(String settingId) {
|
String? getSettingString(String settingId) {
|
||||||
return prefs?.getString(settingId);
|
return prefs?.getString(settingId);
|
||||||
}
|
}
|
||||||
@ -160,7 +220,22 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
Map<String, int> get categories =>
|
Map<String, int> get categories =>
|
||||||
Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}'));
|
Map<String, int>.from(jsonDecode(prefs?.getString('categories') ?? '{}'));
|
||||||
|
|
||||||
set categories(Map<String, int> cats) {
|
void setCategories(Map<String, int> cats, {AppsProvider? appsProvider}) {
|
||||||
|
if (appsProvider != null) {
|
||||||
|
List<App> changedApps = appsProvider
|
||||||
|
.getAppValues()
|
||||||
|
.map((a) {
|
||||||
|
var n1 = a.app.categories.length;
|
||||||
|
a.app.categories.removeWhere((c) => !cats.keys.contains(c));
|
||||||
|
return n1 > a.app.categories.length ? a.app : null;
|
||||||
|
})
|
||||||
|
.where((element) => element != null)
|
||||||
|
.map((e) => e as App)
|
||||||
|
.toList();
|
||||||
|
if (changedApps.isNotEmpty) {
|
||||||
|
appsProvider.saveApps(changedApps);
|
||||||
|
}
|
||||||
|
}
|
||||||
prefs?.setString('categories', jsonEncode(cats));
|
prefs?.setString('categories', jsonEncode(cats));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -168,7 +243,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
String? get forcedLocale {
|
String? get forcedLocale {
|
||||||
var fl = prefs?.getString('forcedLocale');
|
var fl = prefs?.getString('forcedLocale');
|
||||||
return supportedLocales
|
return supportedLocales
|
||||||
.where((element) => element.toLanguageTag() == fl)
|
.where((element) => element.key.toLanguageTag() == fl)
|
||||||
.isNotEmpty
|
.isNotEmpty
|
||||||
? fl
|
? fl
|
||||||
: null;
|
: null;
|
||||||
@ -178,7 +253,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
if (fl == null) {
|
if (fl == null) {
|
||||||
prefs?.remove('forcedLocale');
|
prefs?.remove('forcedLocale');
|
||||||
} else if (supportedLocales
|
} else if (supportedLocales
|
||||||
.where((element) => element.toLanguageTag() == fl)
|
.where((element) => element.key.toLanguageTag() == fl)
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
prefs?.setString('forcedLocale', fl);
|
prefs?.setString('forcedLocale', fl);
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
|
import 'package:obtainium/app_sources/apkpure.dart';
|
||||||
import 'package:obtainium/app_sources/codeberg.dart';
|
import 'package:obtainium/app_sources/codeberg.dart';
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||||
@ -14,10 +16,12 @@ import 'package:obtainium/app_sources/github.dart';
|
|||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
|
import 'package:obtainium/app_sources/jenkins.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/neutroncode.dart';
|
import 'package:obtainium/app_sources/neutroncode.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
import 'package:obtainium/app_sources/sourceforge.dart';
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
|
import 'package:obtainium/app_sources/sourcehut.dart';
|
||||||
import 'package:obtainium/app_sources/steammobile.dart';
|
import 'package:obtainium/app_sources/steammobile.dart';
|
||||||
import 'package:obtainium/app_sources/telegramapp.dart';
|
import 'package:obtainium/app_sources/telegramapp.dart';
|
||||||
import 'package:obtainium/app_sources/vlc.dart';
|
import 'package:obtainium/app_sources/vlc.dart';
|
||||||
@ -34,7 +38,7 @@ class AppNames {
|
|||||||
|
|
||||||
class APKDetails {
|
class APKDetails {
|
||||||
late String version;
|
late String version;
|
||||||
late List<String> apkUrls;
|
late List<MapEntry<String, String>> apkUrls;
|
||||||
late AppNames names;
|
late AppNames names;
|
||||||
late DateTime? releaseDate;
|
late DateTime? releaseDate;
|
||||||
late String? changeLog;
|
late String? changeLog;
|
||||||
@ -43,6 +47,106 @@ class APKDetails {
|
|||||||
{this.releaseDate, this.changeLog});
|
{this.releaseDate, this.changeLog});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||||
|
mapList.map((e) => [e.key, e.value]).toList();
|
||||||
|
|
||||||
|
assumed2DlistToStringMapList(List<dynamic> arr) =>
|
||||||
|
arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
|
||||||
|
|
||||||
|
// App JSON schema has changed multiple times over the many versions of Obtainium
|
||||||
|
// This function takes an App JSON and modifies it if needed to conform to the latest (current) version
|
||||||
|
appJSONCompatibilityModifiers(Map<String, dynamic> json) {
|
||||||
|
var source = SourceProvider()
|
||||||
|
.getSource(json['url'], overrideSource: json['overrideSource']);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// Convert bool style version detection options to dropdown style
|
||||||
|
if (additionalSettings['noVersionDetection'] == true) {
|
||||||
|
additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||||
|
if (additionalSettings['releaseDateAsVersion'] == true) {
|
||||||
|
additionalSettings['versionDetection'] = 'releaseDateAsVersion';
|
||||||
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
|
}
|
||||||
|
if (additionalSettings['noVersionDetection'] != null) {
|
||||||
|
additionalSettings.remove('noVersionDetection');
|
||||||
|
}
|
||||||
|
if (additionalSettings['releaseDateAsVersion'] != null) {
|
||||||
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure additionalSettings are correctly typed
|
||||||
|
for (var item in formItems) {
|
||||||
|
if (additionalSettings[item.key] != null) {
|
||||||
|
additionalSettings[item.key] =
|
||||||
|
item.ensureType(additionalSettings[item.key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int preferredApkIndex =
|
||||||
|
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int;
|
||||||
|
if (preferredApkIndex < 0) {
|
||||||
|
preferredApkIndex = 0;
|
||||||
|
}
|
||||||
|
json['preferredApkIndex'] = preferredApkIndex;
|
||||||
|
// apkUrls can either be old list or new named list apkUrls
|
||||||
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
|
if (json['apkUrls'] != null) {
|
||||||
|
var apkUrlJson = jsonDecode(json['apkUrls']);
|
||||||
|
try {
|
||||||
|
apkUrls = getApkUrlsFromUrls(List<String>.from(apkUrlJson));
|
||||||
|
} catch (e) {
|
||||||
|
apkUrls = assumed2DlistToStringMapList(List<dynamic>.from(apkUrlJson));
|
||||||
|
apkUrls = List<dynamic>.from(apkUrlJson)
|
||||||
|
.map((e) => MapEntry(e[0] as String, e[1] as String))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
json['apkUrls'] = jsonEncode(stringMapListTo2DList(apkUrls));
|
||||||
|
}
|
||||||
|
// Arch based APK filter option should be disabled if it previously did not exist
|
||||||
|
if (additionalSettings['autoApkFilterByArch'] == null) {
|
||||||
|
additionalSettings['autoApkFilterByArch'] = false;
|
||||||
|
}
|
||||||
|
json['additionalSettings'] = jsonEncode(additionalSettings);
|
||||||
|
// F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
|
||||||
|
// This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
|
||||||
|
// While not causing problems for existing apps from that source that were added in a previous version
|
||||||
|
var overrideSourceWasUndefined = !json.keys.contains('overrideSource');
|
||||||
|
if ((json['url'] as String).startsWith('https://cloudflare.f-droid.org')) {
|
||||||
|
json['overrideSource'] = FDroid().runtimeType.toString();
|
||||||
|
} else if (overrideSourceWasUndefined) {
|
||||||
|
// Similar to above, but for third-party F-Droid repos
|
||||||
|
RegExpMatch? match = RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)')
|
||||||
|
.firstMatch(json['url'] as String);
|
||||||
|
if (match != null) {
|
||||||
|
json['overrideSource'] = FDroidRepo().runtimeType.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
late String id;
|
late String id;
|
||||||
late String url;
|
late String url;
|
||||||
@ -50,7 +154,7 @@ class App {
|
|||||||
late String name;
|
late String name;
|
||||||
String? installedVersion;
|
String? installedVersion;
|
||||||
late String latestVersion;
|
late String latestVersion;
|
||||||
List<String> apkUrls = [];
|
List<MapEntry<String, String>> apkUrls = [];
|
||||||
late int preferredApkIndex;
|
late int preferredApkIndex;
|
||||||
late Map<String, dynamic> additionalSettings;
|
late Map<String, dynamic> additionalSettings;
|
||||||
late DateTime? lastUpdateCheck;
|
late DateTime? lastUpdateCheck;
|
||||||
@ -58,6 +162,8 @@ class App {
|
|||||||
List<String> categories;
|
List<String> categories;
|
||||||
late DateTime? releaseDate;
|
late DateTime? releaseDate;
|
||||||
late String? changeLog;
|
late String? changeLog;
|
||||||
|
late String? overrideSource;
|
||||||
|
bool allowIdChange = false;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -72,68 +178,44 @@ class App {
|
|||||||
this.pinned,
|
this.pinned,
|
||||||
{this.categories = const [],
|
{this.categories = const [],
|
||||||
this.releaseDate,
|
this.releaseDate,
|
||||||
this.changeLog});
|
this.changeLog,
|
||||||
|
this.overrideSource,
|
||||||
|
this.allowIdChange = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALSETTINGS: ${additionalSettings.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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get overrideName =>
|
||||||
|
additionalSettings['appName']?.toString().trim().isNotEmpty == true
|
||||||
|
? additionalSettings['appName']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
String get finalName {
|
||||||
|
return overrideName ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
App deepCopy() => App(
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
author,
|
||||||
|
name,
|
||||||
|
installedVersion,
|
||||||
|
latestVersion,
|
||||||
|
apkUrls,
|
||||||
|
preferredApkIndex,
|
||||||
|
Map.from(additionalSettings),
|
||||||
|
lastUpdateCheck,
|
||||||
|
pinned,
|
||||||
|
categories: categories,
|
||||||
|
changeLog: changeLog,
|
||||||
|
releaseDate: releaseDate,
|
||||||
|
overrideSource: overrideSource,
|
||||||
|
allowIdChange: allowIdChange);
|
||||||
|
|
||||||
factory App.fromJson(Map<String, dynamic> json) {
|
factory App.fromJson(Map<String, dynamic> json) {
|
||||||
var source = SourceProvider().getSource(json['url']);
|
json = appJSONCompatibilityModifiers(json);
|
||||||
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;
|
|
||||||
}
|
|
||||||
// Convert bool style version detection options to dropdown style
|
|
||||||
if (additionalSettings['noVersionDetection'] == true) {
|
|
||||||
additionalSettings['versionDetection'] = 'noVersionDetection';
|
|
||||||
if (additionalSettings['releaseDateAsVersion'] == true) {
|
|
||||||
additionalSettings['versionDetection'] = 'releaseDateAsVersion';
|
|
||||||
additionalSettings.remove('releaseDateAsVersion');
|
|
||||||
}
|
|
||||||
if (additionalSettings['noVersionDetection'] != null) {
|
|
||||||
additionalSettings.remove('noVersionDetection');
|
|
||||||
}
|
|
||||||
if (additionalSettings['releaseDateAsVersion'] != null) {
|
|
||||||
additionalSettings.remove('releaseDateAsVersion');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure additionalSettings are correctly typed
|
|
||||||
for (var item in formItems) {
|
|
||||||
if (additionalSettings[item.key] != null) {
|
|
||||||
additionalSettings[item.key] =
|
|
||||||
item.ensureType(additionalSettings[item.key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int preferredApkIndex = json['preferredApkIndex'] == null
|
|
||||||
? 0
|
|
||||||
: json['preferredApkIndex'] as int;
|
|
||||||
if (preferredApkIndex < 0) {
|
|
||||||
preferredApkIndex = 0;
|
|
||||||
}
|
|
||||||
return App(
|
return App(
|
||||||
json['id'] as String,
|
json['id'] as String,
|
||||||
json['url'] as String,
|
json['url'] as String,
|
||||||
@ -143,11 +225,9 @@ class App {
|
|||||||
? null
|
? null
|
||||||
: json['installedVersion'] as String,
|
: json['installedVersion'] as String,
|
||||||
json['latestVersion'] as String,
|
json['latestVersion'] as String,
|
||||||
json['apkUrls'] == null
|
assumed2DlistToStringMapList(jsonDecode(json['apkUrls'])),
|
||||||
? []
|
json['preferredApkIndex'] as int,
|
||||||
: List<String>.from(jsonDecode(json['apkUrls'])),
|
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||||
preferredApkIndex,
|
|
||||||
additionalSettings,
|
|
||||||
json['lastUpdateCheck'] == null
|
json['lastUpdateCheck'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||||
@ -163,7 +243,9 @@ class App {
|
|||||||
? null
|
? null
|
||||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||||
changeLog:
|
changeLog:
|
||||||
json['changeLog'] == null ? null : json['changeLog'] as String);
|
json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||||
|
overrideSource: json['overrideSource'],
|
||||||
|
allowIdChange: json['allowIdChange'] ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -173,14 +255,16 @@ class App {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'installedVersion': installedVersion,
|
'installedVersion': installedVersion,
|
||||||
'latestVersion': latestVersion,
|
'latestVersion': latestVersion,
|
||||||
'apkUrls': jsonEncode(apkUrls),
|
'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
|
||||||
'preferredApkIndex': preferredApkIndex,
|
'preferredApkIndex': preferredApkIndex,
|
||||||
'additionalSettings': jsonEncode(additionalSettings),
|
'additionalSettings': jsonEncode(additionalSettings),
|
||||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||||
'pinned': pinned,
|
'pinned': pinned,
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
'releaseDate': releaseDate?.microsecondsSinceEpoch,
|
||||||
'changeLog': changeLog
|
'changeLog': changeLog,
|
||||||
|
'overrideSource': overrideSource,
|
||||||
|
'allowIdChange': allowIdChange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,17 +309,62 @@ Map<String, dynamic> getDefaultValuesFromFormItems(
|
|||||||
.reduce((value, element) => [...value, ...element]));
|
.reduce((value, element) => [...value, ...element]));
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppSource {
|
List<MapEntry<String, String>> getApkUrlsFromUrls(List<String> urls) =>
|
||||||
|
urls.map((e) {
|
||||||
|
var segments = e.split('/').where((el) => el.trim().isNotEmpty);
|
||||||
|
var apkSegs = segments.where((s) => s.toLowerCase().endsWith('.apk'));
|
||||||
|
return MapEntry(apkSegs.isNotEmpty ? apkSegs.last : segments.last, e);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
abstract class AppSource {
|
||||||
String? host;
|
String? host;
|
||||||
|
bool hostChanged = false;
|
||||||
late String name;
|
late String name;
|
||||||
bool enforceTrackOnly = false;
|
bool enforceTrackOnly = false;
|
||||||
bool changeLogIfAnyIsMarkDown = true;
|
bool changeLogIfAnyIsMarkDown = true;
|
||||||
|
bool appIdInferIsOptional = false;
|
||||||
|
|
||||||
AppSource() {
|
AppSource() {
|
||||||
name = runtimeType.toString();
|
name = runtimeType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String standardizeURL(String url) {
|
overrideVersionDetectionFormDefault(String vd, bool disableStandard) {
|
||||||
|
additionalAppSpecificSourceAgnosticSettingFormItems =
|
||||||
|
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
|
||||||
|
return e.map((e2) {
|
||||||
|
if (e2.key == 'versionDetection') {
|
||||||
|
var item = e2 as GeneratedFormDropdown;
|
||||||
|
item.defaultValue = vd;
|
||||||
|
if (disableStandard) {
|
||||||
|
item.disabledOptKeys = ['standardVersionDetection'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e2;
|
||||||
|
}).toList();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String standardizeUrl(String url) {
|
||||||
|
url = preStandardizeUrl(url);
|
||||||
|
if (!hostChanged) {
|
||||||
|
url = sourceSpecificStandardizeURL(url);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String>? get requestHeaders => null;
|
||||||
|
|
||||||
|
Future<Response> sourceRequest(String url) async {
|
||||||
|
if (requestHeaders != null) {
|
||||||
|
var req = Request('GET', Uri.parse(url));
|
||||||
|
req.headers.addAll(requestHeaders!);
|
||||||
|
return Response.fromStream(await Client().send(req));
|
||||||
|
} else {
|
||||||
|
return get(Uri.parse(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +378,7 @@ class AppSource {
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
// Some additional data may be needed for Apps regardless of Source
|
// Some additional data may be needed for Apps regardless of Source
|
||||||
final List<List<GeneratedFormItem>>
|
List<List<GeneratedFormItem>>
|
||||||
additionalAppSpecificSourceAgnosticSettingFormItems = [
|
additionalAppSpecificSourceAgnosticSettingFormItems = [
|
||||||
[
|
[
|
||||||
GeneratedFormSwitch(
|
GeneratedFormSwitch(
|
||||||
@ -278,7 +407,12 @@ class AppSource {
|
|||||||
return regExValidator(value);
|
return regExValidator(value);
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('autoApkFilterByArch',
|
||||||
|
label: tr('autoApkFilterByArch'), defaultValue: true)
|
||||||
|
],
|
||||||
|
[GeneratedFormTextField('appName', label: tr('appName'), required: false)]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Previous 2 variables combined into one at runtime for convenient usage
|
// Previous 2 variables combined into one at runtime for convenient usage
|
||||||
@ -296,17 +430,18 @@ class AppSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
Future<String> apkUrlPrefetchModifier(
|
||||||
|
String apkUrl, String standardUrl) async {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canSearch = false;
|
bool canSearch = false;
|
||||||
Future<Map<String, String>> search(String query) {
|
Future<Map<String, List<String>>> search(String query) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
String? tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,7 +454,7 @@ ObtainiumError getObtainiumHttpError(Response res) {
|
|||||||
abstract class MassAppUrlSource {
|
abstract class MassAppUrlSource {
|
||||||
late String name;
|
late String name;
|
||||||
late List<String> requiredArgs;
|
late List<String> requiredArgs;
|
||||||
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
Future<Map<String, List<String>>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
regExValidator(String? value) {
|
regExValidator(String? value) {
|
||||||
@ -336,33 +471,48 @@ regExValidator(String? value) {
|
|||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
List<AppSource> sources = [
|
List<AppSource> get sources => [
|
||||||
GitHub(),
|
GitHub(),
|
||||||
GitLab(),
|
GitLab(),
|
||||||
Codeberg(),
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
SourceForge(),
|
Jenkins(),
|
||||||
APKMirror(),
|
SourceForge(),
|
||||||
Mullvad(),
|
SourceHut(),
|
||||||
Signal(),
|
APKMirror(),
|
||||||
VLC(),
|
APKPure(),
|
||||||
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
|
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||||
TelegramApp(),
|
Mullvad(),
|
||||||
SteamMobile(),
|
Signal(),
|
||||||
NeutronCode(),
|
VLC(),
|
||||||
HTML() // This should ALWAYS be the last option as they are tried in order
|
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
|
||||||
];
|
TelegramApp(),
|
||||||
|
SteamMobile(),
|
||||||
|
NeutronCode(),
|
||||||
|
HTML() // This should ALWAYS be the last option as they are tried in order
|
||||||
|
];
|
||||||
|
|
||||||
// Add more mass url source classes here so they are available via the service
|
// Add more mass url source classes here so they are available via the service
|
||||||
List<MassAppUrlSource> massUrlSources = [GitHubStars()];
|
List<MassAppUrlSource> massUrlSources = [GitHubStars()];
|
||||||
|
|
||||||
AppSource getSource(String url) {
|
AppSource getSource(String url, {String? overrideSource}) {
|
||||||
url = preStandardizeUrl(url);
|
url = preStandardizeUrl(url);
|
||||||
|
if (overrideSource != null) {
|
||||||
|
var srcs =
|
||||||
|
sources.where((e) => e.runtimeType.toString() == overrideSource);
|
||||||
|
if (srcs.isEmpty) {
|
||||||
|
throw UnsupportedURLError();
|
||||||
|
}
|
||||||
|
var res = srcs.first;
|
||||||
|
res.host = Uri.parse(url).host;
|
||||||
|
res.hostChanged = true;
|
||||||
|
return srcs.first;
|
||||||
|
}
|
||||||
AppSource? source;
|
AppSource? source;
|
||||||
for (var s in sources.where((element) => element.host != null)) {
|
for (var s in sources.where((element) => element.host != null)) {
|
||||||
if (url.contains('://${s.host}')) {
|
if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) {
|
||||||
source = s;
|
source = s;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -370,7 +520,7 @@ class SourceProvider {
|
|||||||
if (source == null) {
|
if (source == null) {
|
||||||
for (var s in sources.where((element) => element.host == null)) {
|
for (var s in sources.where((element) => element.host == null)) {
|
||||||
try {
|
try {
|
||||||
s.standardizeURL(url);
|
s.sourceSpecificStandardizeURL(url);
|
||||||
source = s;
|
source = s;
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -406,12 +556,15 @@ class SourceProvider {
|
|||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(
|
||||||
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
AppSource source, String url, Map<String, dynamic> additionalSettings,
|
||||||
{App? currentApp, bool trackOnlyOverride = false}) async {
|
{App? currentApp,
|
||||||
|
bool trackOnlyOverride = false,
|
||||||
|
String? overrideSource,
|
||||||
|
bool inferAppIdIfOptional = false}) async {
|
||||||
if (trackOnlyOverride || source.enforceTrackOnly) {
|
if (trackOnlyOverride || source.enforceTrackOnly) {
|
||||||
additionalSettings['trackOnly'] = true;
|
additionalSettings['trackOnly'] = true;
|
||||||
}
|
}
|
||||||
var trackOnly = additionalSettings['trackOnly'] == true;
|
var trackOnly = additionalSettings['trackOnly'] == true;
|
||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeUrl(url);
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
await source.getLatestAPKDetails(standardUrl, additionalSettings);
|
||||||
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
||||||
@ -421,24 +574,40 @@ class SourceProvider {
|
|||||||
if (additionalSettings['apkFilterRegEx'] != null) {
|
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||||
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||||
apk.apkUrls =
|
apk.apkUrls =
|
||||||
apk.apkUrls.where((element) => reg.hasMatch(element)).toList();
|
apk.apkUrls.where((element) => reg.hasMatch(element.key)).toList();
|
||||||
}
|
}
|
||||||
if (apk.apkUrls.isEmpty && !trackOnly) {
|
if (apk.apkUrls.isEmpty && !trackOnly) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
}
|
}
|
||||||
|
if (apk.apkUrls.length > 1 &&
|
||||||
|
additionalSettings['autoApkFilterByArch'] == true) {
|
||||||
|
var abis = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||||
|
for (var abi in abis) {
|
||||||
|
var urls2 = apk.apkUrls
|
||||||
|
.where((element) => RegExp('.*$abi.*').hasMatch(element.key))
|
||||||
|
.toList();
|
||||||
|
if (urls2.isNotEmpty && urls2.length < apk.apkUrls.length) {
|
||||||
|
apk.apkUrls = urls2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
String apkVersion = apk.version.replaceAll('/', '-');
|
String apkVersion = apk.version.replaceAll('/', '-');
|
||||||
var name = currentApp?.name.trim() ??
|
var name = currentApp != null ? currentApp.name.trim() : '';
|
||||||
apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
|
name = name.isNotEmpty
|
||||||
|
? name
|
||||||
|
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1);
|
||||||
return App(
|
return App(
|
||||||
currentApp?.id ??
|
currentApp?.id ??
|
||||||
source.tryInferringAppId(standardUrl,
|
((!source.appIdInferIsOptional ||
|
||||||
additionalSettings: additionalSettings) ??
|
(source.appIdInferIsOptional && inferAppIdIfOptional))
|
||||||
|
? await source.tryInferringAppId(standardUrl,
|
||||||
|
additionalSettings: additionalSettings)
|
||||||
|
: null) ??
|
||||||
generateTempID(standardUrl, additionalSettings),
|
generateTempID(standardUrl, additionalSettings),
|
||||||
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,
|
||||||
? name
|
|
||||||
: apk.names.name[0].toUpperCase() + apk.names.name.substring(1),
|
|
||||||
currentApp?.installedVersion,
|
currentApp?.installedVersion,
|
||||||
apkVersion,
|
apkVersion,
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
@ -448,16 +617,24 @@ class SourceProvider {
|
|||||||
currentApp?.pinned ?? false,
|
currentApp?.pinned ?? false,
|
||||||
categories: currentApp?.categories ?? const [],
|
categories: currentApp?.categories ?? const [],
|
||||||
releaseDate: apk.releaseDate,
|
releaseDate: apk.releaseDate,
|
||||||
changeLog: apk.changeLog);
|
changeLog: apk.changeLog,
|
||||||
|
overrideSource: overrideSource ?? currentApp?.overrideSource,
|
||||||
|
allowIdChange: currentApp?.allowIdChange ??
|
||||||
|
source.appIdInferIsOptional &&
|
||||||
|
inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns errors in [results, errors] instead of throwing them
|
// Returns errors in [results, errors] instead of throwing them
|
||||||
Future<List<dynamic>> getAppsByURLNaive(List<String> urls,
|
Future<List<dynamic>> getAppsByURLNaive(List<String> urls,
|
||||||
{List<String> ignoreUrls = const []}) async {
|
{List<String> alreadyAddedUrls = const []}) async {
|
||||||
List<App> apps = [];
|
List<App> apps = [];
|
||||||
Map<String, dynamic> errors = {};
|
Map<String, dynamic> errors = {};
|
||||||
for (var url in urls.where((element) => !ignoreUrls.contains(element))) {
|
for (var url in urls) {
|
||||||
try {
|
try {
|
||||||
|
if (alreadyAddedUrls.contains(url)) {
|
||||||
|
throw ObtainiumError(tr('appAlreadyAdded'));
|
||||||
|
}
|
||||||
var source = getSource(url);
|
var source = getSource(url);
|
||||||
apps.add(await getApp(
|
apps.add(await getApp(
|
||||||
source,
|
source,
|
||||||
|
383
pubspec.lock
383
pubspec.lock
@ -5,18 +5,27 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_alarm_manager_plus
|
name: android_alarm_manager_plus
|
||||||
sha256: "8647cc5f9339f3955a2bd9ec40e0f10c3a80049f31f80b3ffdd87e07bb73fce2"
|
sha256: "80f963d47cb7ab0818144c7b0668aea4c038f9cb8626626e89a4ea77375defb7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "3.0.1"
|
||||||
android_intent_plus:
|
android_intent_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_intent_plus
|
name: android_intent_plus
|
||||||
sha256: "54810cb33945c2c10742cd746ea994822c115e9dbe189919bc63cb436e45a6af"
|
sha256: "2c87d8330ba5deef5fe20e77f4d178190b3b24531dce08368030ab4be40a9d4e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "4.0.1"
|
||||||
|
android_package_installer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: main
|
||||||
|
resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1
|
||||||
|
url: "https://github.com/ImranR98/android_package_installer"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -25,22 +34,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.2"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.0"
|
version: "2.11.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -53,10 +70,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.3.0"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -69,10 +102,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -85,18 +126,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.3"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: csslib
|
name: csslib
|
||||||
sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745
|
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.2"
|
version: "1.0.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -117,10 +158,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95"
|
sha256: "2c35b6d1682b028e42d07b3aee4b98fa62996c10bc12cb651ec856a80d6a761b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.0"
|
version: "9.0.2"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -133,18 +174,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dynamic_color
|
name: dynamic_color
|
||||||
sha256: c4a508284b14ec4dda5adba2c28b2cdd34fbae1afead7e8c52cad87d51c5405b
|
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.2"
|
version: "1.6.6"
|
||||||
easy_localization:
|
easy_localization:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: easy_localization
|
name: easy_localization
|
||||||
sha256: "6a2e99fa0bfe5765bf4c6ca9b137d5de2c75593007178c5e4cd2ae985f870080"
|
sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
easy_logger:
|
easy_logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -165,10 +206,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -181,15 +222,23 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: d8e9ca7e5d1983365c277f12c21b4362df6cf659c99af146ad4d04eb33033013
|
sha256: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.6"
|
version: "5.3.2"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_archive
|
||||||
|
sha256: aec85d1da65e5b33a529db00a86df0b8e92bda78088a7cfaeeba5187701d0d85
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
flutter_fgbg:
|
flutter_fgbg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -198,38 +247,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
|
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0"
|
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "15.1.0+1"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_linux
|
name: flutter_local_notifications_linux
|
||||||
sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89
|
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0+1"
|
version: "4.0.0+1"
|
||||||
flutter_local_notifications_platform_interface:
|
flutter_local_notifications_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_platform_interface
|
name: flutter_local_notifications_platform_interface
|
||||||
sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab"
|
sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "7.0.0+1"
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -239,18 +296,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f"
|
sha256: "4b1bfbb802d76320a1a46d9ce984106135093efd9d969765d07c2125af107bdf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.14"
|
version: "0.6.17"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf
|
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.9"
|
version: "2.0.15"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -265,26 +322,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25"
|
sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.1"
|
version: "8.2.2"
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb"
|
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.2"
|
version: "0.15.4"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
|
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.5"
|
version: "1.1.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -293,14 +350,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
install_plugin_v2:
|
image:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: install_plugin_v2
|
name: image
|
||||||
sha256: d6b014637e7a53839e9c5a254f9fd9bb8866392c6db1f16184ce17818cc2d979
|
sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "4.0.17"
|
||||||
installed_apps:
|
installed_apps:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -313,42 +370,50 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.0"
|
version: "0.18.0"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: js
|
name: js
|
||||||
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.5"
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.1.1"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: markdown
|
name: markdown
|
||||||
sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b
|
sha256: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.1.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
|
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.13"
|
version: "0.12.15"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -361,10 +426,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
|
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.9.1"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -401,42 +466,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.3"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.14"
|
version: "2.0.15"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7"
|
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.24"
|
version: "2.0.27"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7"
|
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.3"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
|
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.10"
|
version: "2.1.11"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -449,58 +514,58 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
|
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.7"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: permission_handler
|
name: permission_handler
|
||||||
sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
|
sha256: "415af30ba76a84faccfe1eb251fe1e4fdc790f876924c65ad7d6ed7a1404bcd6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.0"
|
version: "10.4.2"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
|
sha256: "3b61f3da3b1c83bc3fb6a2b431e8dab01d0e5b45f6a3d9c7609770ec88b2a89e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.0"
|
version: "10.3.0"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
|
sha256: "7a187b671a39919462af2b5e813148365b71a615979165a119868d667fe90c03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.8"
|
version: "9.1.3"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_platform_interface
|
name: permission_handler_platform_interface
|
||||||
sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84"
|
sha256: "463a07cb7cc6c758a7a1c7da36ce666bb80a0b4b5e92df0fa36872e0ed456993"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.0"
|
version: "3.11.1"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_windows
|
name: permission_handler_windows
|
||||||
sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
|
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.3"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
|
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.4.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -517,6 +582,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.7.3"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -537,74 +610,74 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
|
sha256: ed3fcea4f789ed95913328e629c0c53e69e80e08b6c24542f1b3576046c614e8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.1"
|
version: "7.0.2"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: share_plus_platform_interface
|
name: share_plus_platform_interface
|
||||||
sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1"
|
sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394"
|
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.20"
|
version: "2.2.0"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521
|
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.17"
|
version: "2.2.0"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310"
|
sha256: b046999bf0ff58f04c364491bb803dcfa8f42e47b19c75478f53d323684a8cc1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.3.1"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_linux
|
name: shared_preferences_linux
|
||||||
sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707"
|
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.3.0"
|
||||||
shared_preferences_platform_interface:
|
shared_preferences_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
|
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.3.0"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_web
|
name: shared_preferences_web
|
||||||
sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8"
|
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.2.0"
|
||||||
shared_preferences_windows:
|
shared_preferences_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_windows
|
name: shared_preferences_windows
|
||||||
sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436"
|
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.3.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -622,18 +695,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
sha256: "500d6fec583d2c021f2d25a056d96654f910662c64f836cd2063167b8f1fa758"
|
sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.6"
|
version: "2.2.8+4"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
sha256: "963dad8c4aa2f814ce7d2d5b1da2f36f31bd1a439d8f27e3dc189bb9d26bc684"
|
sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.4.5+1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -662,10 +735,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: synchronized
|
name: synchronized
|
||||||
sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b"
|
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -678,90 +751,90 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
|
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.16"
|
version: "0.5.1"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: timezone
|
name: timezone
|
||||||
sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964"
|
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.1"
|
version: "0.9.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
|
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.10"
|
version: "6.1.12"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8
|
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.26"
|
version: "6.0.36"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92"
|
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.3"
|
version: "6.1.4"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc"
|
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.4"
|
version: "3.0.5"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a"
|
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.4"
|
version: "3.0.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
|
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa"
|
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.16"
|
version: "2.0.18"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_windows
|
name: url_launcher_windows
|
||||||
sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd
|
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.7"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -782,58 +855,74 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter
|
name: webview_flutter
|
||||||
sha256: "47663d51a9061451aa3880a214ee9a65dcbb933b77bc44388e194279ab3ccaf6"
|
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.7"
|
version: "4.2.2"
|
||||||
webview_flutter_android:
|
webview_flutter_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: "34f83c2f0f64c75ad75c77a2ccfc8d2e531afbe8ad41af1fd787d6d33336aa90"
|
sha256: "1c93e96f3069bacdc734fad6b7e1d3a480fd516a3ae5b8858becf7f07515a2f3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.3"
|
version: "3.8.2"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
sha256: "1939c39e2150fb4d30fd3cc59a891a49fed9935db53007df633ed83581b6117b"
|
sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.3.1"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: d601aba11ad8d4481e17a34a76fa1d30dee92dcbbe2c58b0df3120e9453099c7
|
sha256: a8d7e8b4be2a79e83b70235369971ec97d14df4cdbb40d305a8eeae67d8e6432
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.3"
|
version: "3.6.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "5.0.5"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
|
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0+3"
|
version: "1.0.0"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
|
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.3.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.18.2 <3.0.0"
|
dart: ">=3.0.0 <4.0.0"
|
||||||
flutter: ">=3.4.0-17.0.pre"
|
flutter: ">=3.10.0"
|
||||||
|
27
pubspec.yaml
27
pubspec.yaml
@ -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.11.17+139 # When changing this, update the tag in main() accordingly
|
version: 0.13.14+178 # 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,9 +38,9 @@ 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: ^13.0.0
|
flutter_local_notifications: ^15.1.0+1
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
http: ^0.13.5
|
http: ^1.0.0
|
||||||
webview_flutter: ^4.0.0
|
webview_flutter: ^4.0.0
|
||||||
dynamic_color: ^1.5.4
|
dynamic_color: ^1.5.4
|
||||||
html: ^0.15.0
|
html: ^0.15.0
|
||||||
@ -48,23 +48,28 @@ dependencies:
|
|||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
permission_handler: ^10.0.0
|
permission_handler: ^10.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
device_info_plus: ^8.0.0
|
device_info_plus: ^9.0.0
|
||||||
file_picker: ^5.1.0
|
file_picker: ^5.2.10
|
||||||
animations: ^2.0.4
|
animations: ^2.0.4
|
||||||
install_plugin_v2: ^1.0.0
|
android_package_installer:
|
||||||
share_plus: ^6.0.1
|
git:
|
||||||
|
url: https://github.com/ImranR98/android_package_installer
|
||||||
|
ref: main
|
||||||
|
share_plus: ^7.0.0
|
||||||
installed_apps: ^1.3.1
|
installed_apps: ^1.3.1
|
||||||
package_archive_info: ^0.1.0
|
package_archive_info: ^0.1.0
|
||||||
android_alarm_manager_plus: ^2.1.0
|
android_alarm_manager_plus: ^3.0.0
|
||||||
sqflite: ^2.2.0+3
|
sqflite: ^2.2.0+3
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
android_intent_plus: ^3.1.5
|
android_intent_plus: ^4.0.0
|
||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
|
flutter_archive: ^5.0.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
@ -73,6 +78,10 @@ dev_dependencies:
|
|||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: "ic_launcher"
|
||||||
|
image_path: "assets/graphics/icon.png"
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user