mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
29 Commits
v0.12.3-be
...
v0.13.2-be
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -15,7 +15,9 @@ 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
|
||||||
- Jenkins Jobs
|
- Jenkins Jobs
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- [Steam](https://store.steampowered.com/mobile)
|
||||||
|
@ -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",
|
||||||
@ -122,12 +121,12 @@
|
|||||||
"followSystem": "System folgen",
|
"followSystem": "System folgen",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"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",
|
||||||
@ -208,7 +207,7 @@
|
|||||||
"addCategory": "Kategorie hinzufügen",
|
"addCategory": "Kategorie hinzufügen",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"copiedToClipboard": "Copied to Clipboard",
|
"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",
|
||||||
@ -219,7 +218,7 @@
|
|||||||
"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": "Nach Kategorie gruppieren",
|
"groupByCategory": "Nach Kategorie gruppieren",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "Nicht noch einmal zeigen",
|
"dontShowAgain": "Nicht noch einmal zeigen",
|
||||||
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft 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",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "Apps entfernen?"
|
"other": "Apps entfernen?"
|
||||||
@ -254,7 +258,7 @@
|
|||||||
},
|
},
|
||||||
"minute": {
|
"minute": {
|
||||||
"one": "{} Minute",
|
"one": "{} Minute",
|
||||||
"other": "{} Minutes"
|
"other": "{} Minuten"
|
||||||
},
|
},
|
||||||
"hour": {
|
"hour": {
|
||||||
"one": "{} Stunde",
|
"one": "{} Stunde",
|
||||||
|
@ -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,7 +121,7 @@
|
|||||||
"followSystem": "Follow System",
|
"followSystem": "Follow System",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"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",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
|
"dontShowTrackOnlyWarnings": "Don't Show 'Track-Only' Warnings",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin 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",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||||
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
|
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
|
||||||
"githubPATFormat": "nombre_de_usuario:token",
|
"githubPATFormat": "nombre_de_usuario:token",
|
||||||
"githubPATLinkText": "Sobre los TAP de GitHub",
|
|
||||||
"includePrereleases": "Incluir versiones preliminares",
|
"includePrereleases": "Incluir versiones preliminares",
|
||||||
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||||
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "No mostrar de nuevo",
|
"dontShowAgain": "No mostrar de nuevo",
|
||||||
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
|
||||||
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
|
"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",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "¿Eliminar Aplicación?",
|
"one": "¿Eliminar Aplicación?",
|
||||||
"other": "¿Eliminar Aplicaciones?"
|
"other": "¿Eliminar Aplicaciones?"
|
||||||
|
@ -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": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "دوباره این را نشان نده",
|
"dontShowAgain": "دوباره این را نشان نده",
|
||||||
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||||
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||||
|
"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",
|
||||||
"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,7 +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",
|
"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",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin 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",
|
||||||
"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",
|
||||||
@ -227,6 +226,11 @@
|
|||||||
"dontShowAgain": "Ne mutassa ezt újra",
|
"dontShowAgain": "Ne mutassa ezt újra",
|
||||||
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||||
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||||
|
"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",
|
||||||
"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?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"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: username:token",
|
||||||
"githubPATFormat": "username:token",
|
"githubPATFormat": "username: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",
|
||||||
@ -122,7 +121,7 @@
|
|||||||
"followSystem": "Segui sistema",
|
"followSystem": "Segui sistema",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Use pure black dark theme",
|
"useBlackTheme": "Use Pure Black Dark Theme",
|
||||||
"appSortBy": "App ordinate per",
|
"appSortBy": "App ordinate per",
|
||||||
"authorName": "Autore/Nome",
|
"authorName": "Autore/Nome",
|
||||||
"nameAuthor": "Nome/Autore",
|
"nameAuthor": "Nome/Autore",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "Don't show this again",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin 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",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'App?",
|
"one": "Rimuovere l'App?",
|
||||||
"other": "Rimuovere le App?"
|
"other": "Rimuovere le App?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||||
"githubPATFormat": "ユーザー名:トークン",
|
"githubPATFormat": "ユーザー名:トークン",
|
||||||
"githubPATLinkText": "GitHub PATsについて",
|
|
||||||
"includePrereleases": "プレリリースを含む",
|
"includePrereleases": "プレリリースを含む",
|
||||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||||
@ -228,6 +227,11 @@
|
|||||||
"dontShowAgain": "二度と表示しない",
|
"dontShowAgain": "二度と表示しない",
|
||||||
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
"dontShowTrackOnlyWarnings": "「追跡のみ」の警告を表示しない",
|
||||||
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
|
"dontShowAPKOriginWarnings": "APK Originの警告を表示しない",
|
||||||
|
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
|
||||||
|
"gitlabPATLabel": "GitLab パーソナルアクセストークン (検索を有効化する)",
|
||||||
|
"about": "概要",
|
||||||
|
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
|
||||||
|
"checkOnStart": "Check Once on Start",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"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": "使用正则表达式筛选发行标题",
|
||||||
@ -34,7 +33,7 @@
|
|||||||
"githubStarredRepos": "GitHub 已星标仓库",
|
"githubStarredRepos": "GitHub 已星标仓库",
|
||||||
"uname": "用户名",
|
"uname": "用户名",
|
||||||
"wrongArgNum": "参数数量错误",
|
"wrongArgNum": "参数数量错误",
|
||||||
"xIsTrackOnly": "{} 为“仅追踪”模式",
|
"xIsTrackOnly": "{}为“仅追踪”模式",
|
||||||
"source": "源代码",
|
"source": "源代码",
|
||||||
"app": "应用",
|
"app": "应用",
|
||||||
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
"appsFromSourceAreTrackOnly": "此来源的应用为“仅追踪”模式。",
|
||||||
@ -51,8 +50,8 @@
|
|||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"additionalOptsFor": "{} 的更多选项",
|
"additionalOptsFor": "{} 的更多选项",
|
||||||
"supportedSourcesBelow": "支持的来源:",
|
"supportedSourcesBelow": "支持的来源:",
|
||||||
"trackOnlyInBrackets": "(仅追踪)",
|
"trackOnlyInBrackets": "(仅追踪)",
|
||||||
"searchableInBrackets": "(可搜索)",
|
"searchableInBrackets": "(可搜索)",
|
||||||
"appsString": "应用列表",
|
"appsString": "应用列表",
|
||||||
"noApps": "无应用",
|
"noApps": "无应用",
|
||||||
"noAppsForFilter": "没有符合条件的应用",
|
"noAppsForFilter": "没有符合条件的应用",
|
||||||
@ -60,9 +59,9 @@
|
|||||||
"percentProgress": "进度:{}%",
|
"percentProgress": "进度:{}%",
|
||||||
"pleaseWait": "请稍候",
|
"pleaseWait": "请稍候",
|
||||||
"updateAvailable": "更新可用",
|
"updateAvailable": "更新可用",
|
||||||
"estimateInBracketsShort": "(预计)",
|
"estimateInBracketsShort": "(推测)",
|
||||||
"notInstalled": "未安装",
|
"notInstalled": "未安装",
|
||||||
"estimateInBrackets": "(预计)",
|
"estimateInBrackets": "(推测)",
|
||||||
"selectAll": "全选",
|
"selectAll": "全选",
|
||||||
"deselectN": "取消选择 {}",
|
"deselectN": "取消选择 {}",
|
||||||
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
|
||||||
@ -75,8 +74,8 @@
|
|||||||
"installUpdateApps": "安装/更新应用",
|
"installUpdateApps": "安装/更新应用",
|
||||||
"installUpdateSelectedApps": "安装/更新选中的应用",
|
"installUpdateSelectedApps": "安装/更新选中的应用",
|
||||||
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
"markXSelectedAppsAsUpdated": "是否将选中的 {} 个应用标记为已更新?",
|
||||||
"no": "不要",
|
"no": "否",
|
||||||
"yes": "好的",
|
"yes": "是",
|
||||||
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
"markSelectedAppsUpdated": "将选中的应用标记为已更新",
|
||||||
"pinToTop": "置顶",
|
"pinToTop": "置顶",
|
||||||
"unpinFromTop": "取消置顶",
|
"unpinFromTop": "取消置顶",
|
||||||
@ -143,7 +142,7 @@
|
|||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"appNotFound": "未找到应用",
|
"appNotFound": "未找到应用",
|
||||||
"obtainiumExportHyphenatedLowercase": "obtainium-导出",
|
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||||
"pickAnAPK": "选择一个 APK 文件",
|
"pickAnAPK": "选择一个 APK 文件",
|
||||||
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
"appHasMoreThanOnePackage": "{} 有多个架构可用:",
|
||||||
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
"deviceSupportsXArch": "您的设备支持 {} 架构。",
|
||||||
@ -173,13 +172,13 @@
|
|||||||
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
"versionCorrectionDisabled": "禁用版本号更正(插件似乎未起作用)",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"none": "无",
|
"none": "无",
|
||||||
"never": "从不",
|
"never": "从未",
|
||||||
"latestVersionX": "最新版本:{}",
|
"latestVersionX": "最新版本:{}",
|
||||||
"installedVersionX": "当前版本:{}",
|
"installedVersionX": "当前版本:{}",
|
||||||
"lastUpdateCheckX": "上次更新检查:{}",
|
"lastUpdateCheckX": "上次更新检查:{}",
|
||||||
"remove": "删除",
|
"remove": "删除",
|
||||||
"yesMarkUpdated": "是的,标记为已更新",
|
"yesMarkUpdated": "是,标记为已更新",
|
||||||
"fdroid": "F-Droid Official",
|
"fdroid": "F-Droid 官方存储库",
|
||||||
"appIdOrName": "应用 ID 或名称",
|
"appIdOrName": "应用 ID 或名称",
|
||||||
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||||
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||||
@ -194,7 +193,7 @@
|
|||||||
"additionalOptions": "附加选项",
|
"additionalOptions": "附加选项",
|
||||||
"disableVersionDetection": "禁用版本检测",
|
"disableVersionDetection": "禁用版本检测",
|
||||||
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||||
"downloadingX": "正在下载 {}",
|
"downloadingX": "正在下载{}",
|
||||||
"downloadNotifDescription": "提示应用的下载进度",
|
"downloadNotifDescription": "提示应用的下载进度",
|
||||||
"noAPKFound": "未找到 APK 文件",
|
"noAPKFound": "未找到 APK 文件",
|
||||||
"noVersionDetection": "禁用版本检测",
|
"noVersionDetection": "禁用版本检测",
|
||||||
@ -223,11 +222,16 @@
|
|||||||
"versionDetection": "版本检测",
|
"versionDetection": "版本检测",
|
||||||
"standardVersionDetection": "常规版本检测",
|
"standardVersionDetection": "常规版本检测",
|
||||||
"groupByCategory": "按类别分组显示",
|
"groupByCategory": "按类别分组显示",
|
||||||
"autoApkFilterByArch": "如果可能,尝试按 CPU 架构筛选 APK 文件",
|
"autoApkFilterByArch": "如果可能,尝试按设备支持的 CPU 架构筛选 APK 文件",
|
||||||
"overrideSource": "Override Source",
|
"overrideSource": "覆盖来源",
|
||||||
"dontShowAgain": "Don't show this again",
|
"dontShowAgain": "不再显示",
|
||||||
"dontShowTrackOnlyWarnings": "Don't Show the 'Track-Only' Warning",
|
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
|
||||||
"dontShowAPKOriginWarnings": "Don't Show APK Origin Warnings",
|
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
|
||||||
|
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||||
|
"gitlabPATLabel": "GitLab 个人访问令牌(用于搜索)",
|
||||||
|
"about": "相关文档",
|
||||||
|
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
|
||||||
|
"checkOnStart": "启动时进行一次检查",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "是否删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "是否删除应用?"
|
"other": "是否删除应用?"
|
||||||
|
116
lib/app_sources/apkcombo.dart
Normal file
116
lib/app_sources/apkcombo.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.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
|
||||||
|
String? tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
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 = tryInferringAppId(standardUrl)!;
|
||||||
|
String host = Uri.parse(standardUrl).host;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
78
lib/app_sources/apkpure.dart
Normal file
78
lib/app_sources/apkpure.dart
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.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
|
||||||
|
String? tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
String appId = tryInferringAppId(standardUrl)!;
|
||||||
|
String host = Uri.parse(standardUrl).host;
|
||||||
|
var res = await sourceRequest('$standardUrl/download');
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var html = parse(res.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;
|
||||||
|
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||||
|
releaseDate: releaseDate);
|
||||||
|
} else {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
class Codeberg extends AppSource {
|
class Codeberg extends AppSource {
|
||||||
Codeberg() {
|
Codeberg() {
|
||||||
host = 'codeberg.org';
|
host = 'codeberg.org';
|
||||||
|
overrideEligible = true;
|
||||||
|
|
||||||
additionalSourceSpecificSettingFormItems = [];
|
additionalSourceSpecificSettingFormItems = [];
|
||||||
|
|
||||||
|
@ -66,14 +66,15 @@ class FDroid extends AppSource {
|
|||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = tryInferringAppId(standardUrl);
|
||||||
String host = Uri.parse(standardUrl).host;
|
String host = Uri.parse(standardUrl).host;
|
||||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await get(Uri.parse('https://$host/api/v1/packages/$appId')),
|
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||||
'https://$host/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl);
|
standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<String>>> search(String query) async {
|
Future<Map<String, List<String>>> search(String query) async {
|
||||||
Response res = await get(Uri.parse('https://search.$host/?q=$query'));
|
Response res = await sourceRequest(
|
||||||
|
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
||||||
|
@ -8,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
class FDroidRepo extends AppSource {
|
class FDroidRepo extends AppSource {
|
||||||
FDroidRepo() {
|
FDroidRepo() {
|
||||||
name = tr('fdroidThirdPartyRepo');
|
name = tr('fdroidThirdPartyRepo');
|
||||||
|
overrideEligible = true;
|
||||||
|
|
||||||
additionalSourceAppSpecificSettingFormItems = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
@ -28,7 +29,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) {
|
||||||
|
@ -2,8 +2,10 @@ 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/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 +13,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';
|
||||||
|
overrideEligible = true;
|
||||||
|
|
||||||
additionalSourceSpecificSettingFormItems = [
|
additionalSourceSpecificSettingFormItems = [
|
||||||
GeneratedFormTextField('github-creds',
|
GeneratedFormTextField('github-creds',
|
||||||
@ -34,7 +37,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 +46,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,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -108,7 +114,7 @@ class GitHub extends AppSource {
|
|||||||
true
|
true
|
||||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(requestUrl));
|
Response res = await sourceRequest(requestUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -129,7 +135,7 @@ class GitHub extends AppSource {
|
|||||||
? DateTime.parse(rel['published_at'])
|
? DateTime.parse(rel['published_at'])
|
||||||
: null;
|
: null;
|
||||||
releases.sort((a, b) {
|
releases.sort((a, b) {
|
||||||
// See #478
|
// See #478 and #534
|
||||||
if (a == b) {
|
if (a == b) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (a == null) {
|
} else if (a == null) {
|
||||||
@ -137,8 +143,19 @@ class GitHub extends AppSource {
|
|||||||
} else if (b == null) {
|
} else if (b == null) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return getReleaseDateFromRelease(a)!
|
var stdFormats = findStandardFormatsForVersion(a['tag_name'], true)
|
||||||
.compareTo(getReleaseDateFromRelease(b)!);
|
.intersection(findStandardFormatsForVersion(b['tag_name'], true));
|
||||||
|
if (stdFormats.isNotEmpty) {
|
||||||
|
var reg = RegExp(stdFormats.first);
|
||||||
|
var matchA = reg.firstMatch(a['tag_name']);
|
||||||
|
var matchB = reg.firstMatch(b['tag_name']);
|
||||||
|
return compareAlphaNumeric(
|
||||||
|
(a['tag_name'] as String).substring(matchA!.start, matchA.end),
|
||||||
|
(b['tag_name'] as String).substring(matchB!.start, matchB.end));
|
||||||
|
} else {
|
||||||
|
return getReleaseDateFromRelease(a)!
|
||||||
|
.compareTo(getReleaseDateFromRelease(b)!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
releases = releases.reversed.toList();
|
releases = releases.reversed.toList();
|
||||||
@ -216,7 +233,7 @@ class GitHub extends AppSource {
|
|||||||
Future<Map<String, List<String>>> searchCommon(
|
Future<Map<String, List<String>>> searchCommon(
|
||||||
String query, String requestUrl, String rootProp,
|
String query, String requestUrl, String rootProp,
|
||||||
{Function(Response)? onHttpErrorCode}) async {
|
{Function(Response)? onHttpErrorCode}) async {
|
||||||
Response res = await get(Uri.parse(requestUrl));
|
Response res = await sourceRequest(requestUrl);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
Map<String, List<String>> urlsWithDescriptions = {};
|
||||||
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {
|
||||||
|
@ -1,14 +1,47 @@
|
|||||||
|
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:obtainium/components/generated_form.dart';
|
||||||
import 'package:easy_localization/easy_localization.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';
|
||||||
|
overrideEligible = true;
|
||||||
|
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 = [
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
[
|
[
|
||||||
@ -28,6 +61,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 = {};
|
||||||
|
json.forEach((element) {
|
||||||
|
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';
|
||||||
@ -39,7 +103,7 @@ class GitLab extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
bool fallbackToOlderReleases =
|
bool fallbackToOlderReleases =
|
||||||
additionalSettings['fallbackToOlderReleases'] == true;
|
additionalSettings['fallbackToOlderReleases'] == true;
|
||||||
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom'));
|
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);
|
||||||
|
@ -4,84 +4,116 @@ 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 {
|
||||||
|
HTML() {
|
||||||
|
overrideEligible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// 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
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl,
|
String standardUrl,
|
||||||
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(
|
links.sort(
|
||||||
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
|
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
|
||||||
@ -95,25 +127,8 @@ class HTML extends AppSource {
|
|||||||
var rel = links.last;
|
var rel = links.last;
|
||||||
var apkName = rel.split('/').last;
|
var apkName = rel.split('/').last;
|
||||||
var version = apkName.substring(0, apkName.length - 4);
|
var version = apkName.substring(0, apkName.length - 4);
|
||||||
List<String> apkUrls = [rel].map((e) {
|
List<String> apkUrls =
|
||||||
try {
|
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
|
||||||
Uri.parse(e).origin;
|
|
||||||
return e;
|
|
||||||
} catch (err) {
|
|
||||||
// is relative
|
|
||||||
}
|
|
||||||
var currPathSegments = uri.path
|
|
||||||
.split('/')
|
|
||||||
.where((element) => element.trim().isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
if (e.startsWith('/') || currPathSegments.isEmpty) {
|
|
||||||
return '${uri.origin}/$e';
|
|
||||||
} else if (e.split('/').length == 1) {
|
|
||||||
return '${uri.origin}/${currPathSegments.join('/')}/$e';
|
|
||||||
} else {
|
|
||||||
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
|
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,8 +31,8 @@ class IzzyOnDroid extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
String? appId = tryInferringAppId(standardUrl);
|
String? appId = 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);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
|
|
||||||
class Jenkins extends AppSource {
|
class Jenkins extends AppSource {
|
||||||
Jenkins() {
|
Jenkins() {
|
||||||
|
overrideEligible = true;
|
||||||
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
|
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ class Jenkins extends AppSource {
|
|||||||
) async {
|
) async {
|
||||||
standardUrl = trimJobUrl(standardUrl);
|
standardUrl = trimJobUrl(standardUrl);
|
||||||
Response res =
|
Response res =
|
||||||
await get(Uri.parse('$standardUrl/lastSuccessfulBuild/api/json'));
|
await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var json = jsonDecode(res.body);
|
var json = jsonDecode(res.body);
|
||||||
var releaseDate = json['timestamp'] == null
|
var releaseDate = json['timestamp'] == null
|
||||||
@ -55,9 +56,6 @@ class Jenkins extends AppSource {
|
|||||||
.where((url) =>
|
.where((url) =>
|
||||||
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
|
||||||
.toList();
|
.toList();
|
||||||
if (apkUrls.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
apkUrls,
|
apkUrls,
|
||||||
|
@ -28,7 +28,7 @@ class Mullvad extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
Response res = await sourceRequest('$standardUrl/en/download/android');
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var versions = parse(res.body)
|
var versions = parse(res.body)
|
||||||
.querySelectorAll('p')
|
.querySelectorAll('p')
|
||||||
|
@ -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;
|
||||||
|
@ -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'];
|
||||||
|
@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
class SourceForge extends AppSource {
|
class SourceForge extends AppSource {
|
||||||
SourceForge() {
|
SourceForge() {
|
||||||
host = 'sourceforge.net';
|
host = 'sourceforge.net';
|
||||||
|
overrideEligible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,7 +30,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 =
|
||||||
|
101
lib/app_sources/sourcehut.dart
Normal file
101
lib/app_sources/sourcehut.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/github.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';
|
||||||
|
overrideEligible = true;
|
||||||
|
|
||||||
|
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;
|
||||||
|
var link = entry.querySelector('link');
|
||||||
|
String releasePage = '$standardUrl/refs/$version';
|
||||||
|
DateTime? releaseDate = releaseDateString != null
|
||||||
|
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
|
||||||
|
: null;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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 =
|
||||||
|
@ -19,8 +19,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 +38,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 = [];
|
||||||
|
@ -14,21 +14,21 @@ class WhatsApp extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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')
|
||||||
|
@ -25,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'));
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
|
|||||||
// ignore: implementation_imports
|
// ignore: implementation_imports
|
||||||
import 'package:easy_localization/src/localization.dart';
|
import 'package:easy_localization/src/localization.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.12.3';
|
const String currentVersion = '0.13.2';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
|
@ -159,9 +159,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
app.preferredApkIndex =
|
app.preferredApkIndex =
|
||||||
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
|
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'));
|
||||||
@ -248,9 +255,18 @@ 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
|
||||||
@ -267,6 +283,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
|
||||||
@ -302,8 +321,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
'overrideSource',
|
'overrideSource',
|
||||||
defaultValue: HTML().runtimeType.toString(),
|
defaultValue: HTML().runtimeType.toString(),
|
||||||
[
|
[
|
||||||
...sourceProvider.sources.map(
|
...sourceProvider.sources
|
||||||
(s) => MapEntry(s.runtimeType.toString(), s.name))
|
.where((s) => s.overrideEligible)
|
||||||
|
.map((s) =>
|
||||||
|
MapEntry(s.runtimeType.toString(), s.name))
|
||||||
],
|
],
|
||||||
label: tr('overrideSource'))
|
label: tr('overrideSource'))
|
||||||
]
|
]
|
||||||
@ -329,8 +350,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 25,
|
height: 16,
|
||||||
),
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
bool shouldShowSearchBar() =>
|
bool shouldShowSearchBar() =>
|
||||||
@ -359,33 +380,33 @@ 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,
|
||||||
),
|
),
|
||||||
if (pickedSourceOverride != null ||
|
|
||||||
pickedSource.runtimeType.toString() ==
|
|
||||||
HTML().runtimeType.toString())
|
|
||||||
getHTMLSourceOverrideDropdown(),
|
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
key: Key(pickedSource.runtimeType.toString()),
|
key: Key(pickedSource.runtimeType.toString()),
|
||||||
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
||||||
@ -459,11 +480,15 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (shouldShowSearchBar())
|
if (pickedSourceOverride != null ||
|
||||||
const SizedBox(
|
(pickedSource != null &&
|
||||||
height: 16,
|
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
|
||||||
|
@ -444,7 +444,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))
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -52,6 +52,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
@ -61,6 +64,27 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var currentFilterIsUpdatesOnly =
|
var currentFilterIsUpdatesOnly =
|
||||||
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = DateTime.now();
|
||||||
|
});
|
||||||
|
return appsProvider.checkUpdates().catchError((e) {
|
||||||
|
showError(e, context);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appsProvider.loadingApps &&
|
||||||
|
appsProvider.apps.isNotEmpty &&
|
||||||
|
settingsProvider.checkJustStarted() &&
|
||||||
|
settingsProvider.checkOnStart) {
|
||||||
|
_refreshIndicatorKey.currentState?.show();
|
||||||
|
}
|
||||||
|
|
||||||
selectedAppIds = selectedAppIds
|
selectedAppIds = selectedAppIds
|
||||||
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
.where((element) => listedApps.map((e) => e.app.id).contains(element))
|
||||||
.toSet();
|
.toSet();
|
||||||
@ -185,6 +209,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) {
|
||||||
@ -303,7 +339,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
?.isBefore(refreshingSince!) ??
|
?.isBefore(refreshingSince!) ??
|
||||||
true))
|
true))
|
||||||
.length /
|
.length /
|
||||||
appsProvider.apps.length,
|
(appsProvider.apps.isNotEmpty ? appsProvider.apps.length : 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
@ -503,10 +539,16 @@ 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: 110,
|
||||||
'100'
|
child: Text(tr('percentProgress', args: [
|
||||||
]))
|
listedApps[index].downloadProgress! >= 0
|
||||||
|
? listedApps[index]
|
||||||
|
.downloadProgress!
|
||||||
|
.toInt()
|
||||||
|
.toString()
|
||||||
|
: tr('pleaseWait')
|
||||||
|
])))
|
||||||
: trailingRow,
|
: trailingRow,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedAppIds.isNotEmpty) {
|
if (selectedAppIds.isNotEmpty) {
|
||||||
@ -1005,19 +1047,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(),
|
||||||
|
@ -205,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>[
|
||||||
@ -217,9 +221,38 @@ 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,
|
||||||
@ -227,7 +260,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('useBlackTheme')),
|
Flexible(child: Text(tr('useBlackTheme'))),
|
||||||
Switch(
|
Switch(
|
||||||
value: settingsProvider.useBlackTheme,
|
value: settingsProvider.useBlackTheme,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -254,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) {
|
||||||
@ -266,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) {
|
||||||
@ -278,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) {
|
||||||
@ -290,7 +337,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('dontShowTrackOnlyWarnings')),
|
Flexible(
|
||||||
|
child:
|
||||||
|
Text(tr('dontShowTrackOnlyWarnings'))),
|
||||||
Switch(
|
Switch(
|
||||||
value:
|
value:
|
||||||
settingsProvider.hideTrackOnlyWarning,
|
settingsProvider.hideTrackOnlyWarning,
|
||||||
@ -304,7 +353,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(tr('dontShowAPKOriginWarnings')),
|
Flexible(
|
||||||
|
child:
|
||||||
|
Text(tr('dontShowAPKOriginWarnings'))),
|
||||||
Switch(
|
Switch(
|
||||||
value:
|
value:
|
||||||
settingsProvider.hideAPKOriginWarning,
|
settingsProvider.hideAPKOriginWarning,
|
||||||
@ -314,31 +365,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(
|
height32,
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
height16,
|
|
||||||
Text(
|
|
||||||
tr('updates'),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
intervalDropdown,
|
|
||||||
const Divider(
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
tr('sourceSpecific'),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
...sourceSpecificFields,
|
|
||||||
const Divider(
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
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,
|
||||||
|
@ -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:archive/archive.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -46,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 = [
|
||||||
@ -114,22 +122,34 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// 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 any partial APKs
|
// Delete any partial APKs
|
||||||
|
var cutoff = DateTime.now().subtract(const Duration(days: 7));
|
||||||
(await getExternalCacheDirectories())
|
(await getExternalCacheDirectories())
|
||||||
?.first
|
?.first
|
||||||
.listSync()
|
.listSync()
|
||||||
.where((element) => element.path.endsWith('.apk.part'))
|
.where((element) =>
|
||||||
|
element.path.endsWith('.part') ||
|
||||||
|
element.statSync().modified.isBefore(cutoff))
|
||||||
.forEach((partialApk) {
|
.forEach((partialApk) {
|
||||||
partialApk.delete();
|
partialApk.delete();
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(String url, String fileName, Function? onProgress,
|
Future<File> downloadFile(
|
||||||
{bool useExisting = true}) async {
|
String url, String fileNameNoExt, Function? onProgress,
|
||||||
|
{bool useExisting = true, Map<String, String>? headers}) async {
|
||||||
var destDir = (await getExternalCacheDirectories())!.first.path;
|
var destDir = (await getExternalCacheDirectories())!.first.path;
|
||||||
StreamedResponse response =
|
var req = Request('GET', Uri.parse(url));
|
||||||
await Client().send(Request('GET', Uri.parse(url)));
|
if (headers != null) {
|
||||||
File downloadedFile = File('$destDir/$fileName');
|
req.headers.addAll(headers);
|
||||||
|
}
|
||||||
|
var client = Client();
|
||||||
|
StreamedResponse response = await client.send(req);
|
||||||
|
var ext = response.headers['content-disposition']!.split('.').last;
|
||||||
|
if (ext.endsWith('"') || ext.endsWith("other")) {
|
||||||
|
ext = ext.substring(0, ext.length - 1);
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
@ -157,11 +177,34 @@ class AppsProvider with ChangeNotifier {
|
|||||||
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) {
|
||||||
|
throw IDChangedError();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downloadedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||||
NotificationsProvider? notificationsProvider =
|
NotificationsProvider? notificationsProvider =
|
||||||
context?.read<NotificationsProvider>();
|
context?.read<NotificationsProvider>();
|
||||||
var notifId = DownloadNotification(app.finalName, 0).id;
|
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||||
@ -170,15 +213,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String downloadUrl = await SourceProvider()
|
AppSource source = SourceProvider()
|
||||||
.getSource(app.url, overrideSource: app.overrideSource)
|
.getSource(app.url, overrideSource: app.overrideSource);
|
||||||
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value);
|
String downloadUrl = await source.apkUrlPrefetchModifier(
|
||||||
var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
|
app.apkUrls[app.preferredApkIndex].value, app.url);
|
||||||
var notif = DownloadNotification(app.finalName, 100);
|
var notif = DownloadNotification(app.finalName, 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;
|
||||||
@ -190,33 +234,45 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
});
|
||||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
// Set to 90 for remaining steps, will make null in 'finally'
|
||||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
if (apps[app.id] != null) {
|
||||||
var newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
apps[app.id]!.downloadProgress = -1;
|
||||||
if (app.id != newInfo.packageName) {
|
notifyListeners();
|
||||||
var isTempId = SourceProvider().isTempId(app);
|
notif = DownloadNotification(app.finalName, -1);
|
||||||
if (apps[app.id] != null && !isTempId) {
|
notificationsProvider?.notify(notif);
|
||||||
throw IDChangedError();
|
|
||||||
}
|
|
||||||
var originalAppId = app.id;
|
|
||||||
app.id = newInfo.packageName;
|
|
||||||
downloadedFile = downloadedFile.renameSync(
|
|
||||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.apk');
|
|
||||||
if (apps[originalAppId] != null) {
|
|
||||||
await removeApps([originalAppId]);
|
|
||||||
await saveApps([app], onlyIfExists: !isTempId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Delete older versions of the APK if any
|
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';
|
||||||
|
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 != downloadedFile.path.split('/').last) {
|
file.path != downloadedFile.path) {
|
||||||
file.delete();
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DownloadedApk(app.id, downloadedFile);
|
if (isAPK) {
|
||||||
|
return DownloadedApk(app.id, downloadedFile);
|
||||||
|
} else {
|
||||||
|
return DownloadedXApkDir(app.id, downloadedFile, xapkDir!);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
notificationsProvider?.cancel(notifId);
|
notificationsProvider?.cancel(notifId);
|
||||||
if (apps[app.id] != null) {
|
if (apps[app.id] != null) {
|
||||||
@ -263,11 +319,43 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
void unzipFile(String filePath, String destinationPath) {
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
final bytes = File(filePath).readAsBytesSync();
|
||||||
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
final archive = ZipDecoder().decodeBytes(bytes);
|
||||||
// But even then, we don't know if it actually succeeded
|
|
||||||
Future<void> installApk(DownloadedApk file, {bool silent = false}) async {
|
for (final file in archive) {
|
||||||
|
final filename = '$destinationPath/${file.name}';
|
||||||
|
if (file.isFile) {
|
||||||
|
final data = file.content as List<int>;
|
||||||
|
File(filename)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(data);
|
||||||
|
} else {
|
||||||
|
Directory(filename).create(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||||
|
{bool silent = false}) async {
|
||||||
|
try {
|
||||||
|
var somethingInstalled = false;
|
||||||
|
for (var apk in dir.extracted
|
||||||
|
.listSync()
|
||||||
|
.where((f) => f is File && f.path.toLowerCase().endsWith('.apk'))) {
|
||||||
|
somethingInstalled = somethingInstalled ||
|
||||||
|
await installApk(DownloadedApk(dir.appId, apk as File),
|
||||||
|
silent: silent);
|
||||||
|
}
|
||||||
|
if (somethingInstalled) {
|
||||||
|
dir.file.delete();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dir.extracted.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||||
// TODO: Use 'silent' when/if ever possible
|
// 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;
|
||||||
@ -283,14 +371,17 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
int? code =
|
int? code =
|
||||||
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
|
||||||
|
bool installed = false;
|
||||||
if (code != null && code != 0 && code != 3) {
|
if (code != null && code != 0 && code != 3) {
|
||||||
throw InstallError(code);
|
throw InstallError(code);
|
||||||
} else if (code == 0) {
|
} else if (code == 0) {
|
||||||
|
installed = true;
|
||||||
apps[file.appId]!.app.installedVersion =
|
apps[file.appId]!.app.installedVersion =
|
||||||
apps[file.appId]!.app.latestVersion;
|
apps[file.appId]!.app.latestVersion;
|
||||||
file.file.delete();
|
file.file.delete();
|
||||||
}
|
}
|
||||||
await saveApps([apps[file.appId]!.app]);
|
await saveApps([apps[file.appId]!.app]);
|
||||||
|
return installed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uninstallApp(String appId) async {
|
void uninstallApp(String appId) async {
|
||||||
@ -416,9 +507,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
for (var id in appsToInstall) {
|
for (var id in appsToInstall) {
|
||||||
try {
|
try {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var downloadedFile = await downloadApp(apps[id]!.app, context);
|
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||||
bool willBeSilent =
|
DownloadedApk? downloadedFile;
|
||||||
await canInstallSilently(apps[downloadedFile.appId]!.app);
|
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
|
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||||
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||||
true)) {
|
true)) {
|
||||||
@ -428,7 +526,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await waitForUserToReturnToForeground(context);
|
await waitForUserToReturnToForeground(context);
|
||||||
}
|
}
|
||||||
await installApk(downloadedFile, silent: willBeSilent);
|
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);
|
installedIds.add(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add(id, e.toString());
|
errors.add(id, e.toString());
|
||||||
@ -680,11 +789,18 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeApps(List<String> appIds) async {
|
Future<void> removeApps(List<String> appIds) async {
|
||||||
|
var apkFiles = (await getExternalCacheDirectories())?.first.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();
|
||||||
}
|
}
|
||||||
|
apkFiles
|
||||||
|
?.where(
|
||||||
|
(element) => element.path.split('/').last.startsWith('$appId-'))
|
||||||
|
.forEach((element) {
|
||||||
|
element.delete();
|
||||||
|
});
|
||||||
if (apps.containsKey(appId)) {
|
if (apps.containsKey(appId)) {
|
||||||
apps.remove(appId);
|
apps.remove(appId);
|
||||||
}
|
}
|
||||||
@ -730,7 +846,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
apps[i].installedVersion = null;
|
apps[i].installedVersion = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await saveApps(apps, attemptToCorrectInstallStatus: !remove);
|
await saveApps(apps, attemptToCorrectInstallStatus: false);
|
||||||
}
|
}
|
||||||
if (remove) {
|
if (remove) {
|
||||||
await removeApps(apps.map((e) => e.id).toList());
|
await removeApps(apps.map((e) => e.id).toList());
|
||||||
|
@ -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,
|
||||||
|
@ -35,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';
|
||||||
|
|
||||||
@ -92,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];
|
||||||
@ -120,6 +130,14 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool checkJustStarted() {
|
||||||
|
if (justStarted) {
|
||||||
|
justStarted = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> getInstallPermission({bool enforce = false}) async {
|
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
|
||||||
@ -154,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;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,9 @@ 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/apkcombo.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';
|
||||||
@ -20,6 +22,7 @@ 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';
|
||||||
@ -315,6 +318,7 @@ abstract class AppSource {
|
|||||||
late String name;
|
late String name;
|
||||||
bool enforceTrackOnly = false;
|
bool enforceTrackOnly = false;
|
||||||
bool changeLogIfAnyIsMarkDown = true;
|
bool changeLogIfAnyIsMarkDown = true;
|
||||||
|
bool overrideEligible = false;
|
||||||
|
|
||||||
AppSource() {
|
AppSource() {
|
||||||
name = runtimeType.toString();
|
name = runtimeType.toString();
|
||||||
@ -344,6 +348,18 @@ abstract class AppSource {
|
|||||||
return 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) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
@ -410,7 +426,8 @@ abstract class AppSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
Future<String> apkUrlPrefetchModifier(
|
||||||
|
String apkUrl, String standardUrl) async {
|
||||||
return apkUrl;
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +476,10 @@ class SourceProvider {
|
|||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
Jenkins(),
|
Jenkins(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
|
SourceHut(),
|
||||||
APKMirror(),
|
APKMirror(),
|
||||||
|
APKPure(),
|
||||||
|
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
VLC(),
|
VLC(),
|
||||||
|
52
pubspec.lock
52
pubspec.lock
@ -34,6 +34,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
archive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -82,6 +90,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
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:
|
||||||
@ -94,10 +110,10 @@ 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:
|
||||||
@ -256,10 +272,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb"
|
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.0.14"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -410,10 +426,10 @@ packages:
|
|||||||
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:
|
||||||
@ -426,10 +442,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.3"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -518,6 +534,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:
|
||||||
@ -554,10 +578,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b"
|
sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -570,10 +594,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
|
sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -623,10 +647,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite
|
name: sqflite
|
||||||
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
sha256: "3a82c9a216b46b88617e3714dd74227eaca20c501c4abcc213e56db26b9caa00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.8"
|
version: "2.2.8+2"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.12.3+163 # When changing this, update the tag in main() accordingly
|
version: 0.13.2+166 # 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'
|
||||||
@ -63,6 +63,7 @@ dependencies:
|
|||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
android_intent_plus: ^3.1.5
|
android_intent_plus: ^3.1.5
|
||||||
flutter_markdown: ^0.6.14
|
flutter_markdown: ^0.6.14
|
||||||
|
archive: ^3.3.7
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user