mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
36 Commits
v0.14.7-be
...
v0.14.13-b
Author | SHA1 | Date | |
---|---|---|---|
17b5604f2a | |||
fdb6eed6d0 | |||
13de0437b8 | |||
a43c45f310 | |||
9c56a4d1fc | |||
2aea1d2631 | |||
118e05a0fa | |||
05f497787e | |||
53cf4d0234 | |||
6e735b1763 | |||
873a1a0683 | |||
27b1149d1c | |||
c1e64f111e | |||
b2af8448fd | |||
8f44338e76 | |||
e4a55abcb3 | |||
d7348b4973 | |||
09421230f2 | |||
4596e32258 | |||
4dc007a4f6 | |||
c53a156969 | |||
94bd0774fb | |||
b178b1d780 | |||
cbc840378c | |||
aa7989c16d | |||
85f9336804 | |||
d66be3ecda | |||
c08e05bd6c | |||
e08ab89fd4 | |||
8ba0a0a776 | |||
73ed0cea88 | |||
58a378d212 | |||
553307ba70 | |||
78f73a9049 | |||
abc69e7a0e | |||
503914dbce |
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -2,31 +2,31 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
about: Something isn't working right.
|
about: Something isn't working right.
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug, To Check
|
labels: bug, to check
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Prerequisites**
|
**Prerequisites**
|
||||||
Please ensure your request is not part of an existing issue.
|
<!-- Please ensure your request is not part of an existing issue. -->
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
<!-- Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Tap on '....'
|
2. Tap on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error -->
|
||||||
|
|
||||||
**Screenshots and Logs**
|
**Screenshots and Logs**
|
||||||
If applicable, add screenshots, logs, and any other artifacts (like some/all files under `/Android/data/dev.imranr.obtainium/`) that you think may help troubleshoot the issue.
|
<!-- If applicable, add screenshots, logs, and any other artifacts (like some/all files under `/Android/data/dev.imranr.obtainium/`) that you think may help troubleshoot the issue. -->
|
||||||
|
|
||||||
**Please complete the following information:**
|
**Please complete the following information:**
|
||||||
- Device: [e.g. Pixel 7]
|
- Device: <!-- [e.g. Pixel 7] -->
|
||||||
- OS: [e.g. GrapheneOS]
|
- OS: <!-- [e.g. GrapheneOS] -->
|
||||||
- Obtainium Version [e.g. 0.14.6-beta]
|
- Obtainium Version: <!-- [e.g. 0.14.6-beta] -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
<!-- Add any other context about the problem here. -->
|
||||||
|
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -2,28 +2,28 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest a new Source, setting, or other feature.
|
about: Suggest a new Source, setting, or other feature.
|
||||||
title: ''
|
title: ''
|
||||||
labels: enhancement, To Check
|
labels: enhancement, to check
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Prerequisites**
|
**Prerequisites**
|
||||||
Please ensure your request is not part of an existing issue.
|
<!-- Please ensure your request is not part of an existing issue. -->
|
||||||
|
|
||||||
**Describe the feature**
|
**Describe the feature**
|
||||||
A clear and concise description of what you want to happen.
|
<!-- A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
For new Sources, it's preferable (not required) if you suggest how the following details can be extracted from the Source in a reliable way (like an API or through web scraping):
|
For new Sources, it's preferable (not required) if you suggest how the following details can be extracted from the Source in a reliable way (like an API or through web scraping):
|
||||||
- The App version (or any release-specific identifier - a "pseudo-version") for the latest release
|
- The App version (or any release-specific identifier - a "pseudo-version") for the latest release
|
||||||
- One or more APK URL(s) for the latest release
|
- One or more APK URL(s) for the latest release
|
||||||
- Above details for previous releases (optional)
|
- Above details for previous releases (optional)
|
||||||
|
|
||||||
Note that the Web scraper cannot deal with JavaScript-enabled content.
|
Note that the Web scraper cannot deal with JavaScript-enabled content. -->
|
||||||
|
|
||||||
**Describe alternatives you've considered (if applicable)**
|
**Describe alternatives you've considered (if applicable)**
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
<!-- A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
Note that app-specific Sources are less likely to be added. In those cases, see if the HTML Source will work for you (if not, see if a generally-applicable enhancement to the HTML Source would work, and suggest that instead).
|
Note that app-specific Sources are less likely to be added. In those cases, see if the HTML Source will work for you (if not, see if a generally-applicable enhancement to the HTML Source would work, and suggest that instead). -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
47
README.md
47
README.md
@ -2,31 +2,36 @@
|
|||||||
|
|
||||||
Get Android App Updates Directly From the Source.
|
Get Android App Updates Directly From the Source.
|
||||||
|
|
||||||
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available.
|
Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.
|
||||||
|
|
||||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
||||||
|
|
||||||
Currently supported App sources:
|
Currently supported App sources:
|
||||||
- [GitHub](https://github.com/)
|
- Open Source - General:
|
||||||
- [GitLab](https://gitlab.com/)
|
- [GitHub](https://github.com/)
|
||||||
- [Codeberg](https://codeberg.org/)
|
- [GitLab](https://gitlab.com/)
|
||||||
- [F-Droid](https://f-droid.org/)
|
- [Codeberg](https://codeberg.org/)
|
||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
- [F-Droid](https://f-droid.org/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- Third Party F-Droid Repos
|
||||||
- [Signal](https://signal.org/)
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
- [SourceHut](https://git.sr.ht/)
|
- [SourceHut](https://git.sr.ht/)
|
||||||
- [Aptoide](https://aptoide.com/)
|
- Other - General:
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKPure](https://apkpure.com/)
|
||||||
- [APKPure](https://apkpure.com/)
|
- [Aptoide](https://aptoide.com/)
|
||||||
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
- [Uptodown](https://uptodown.com/)
|
||||||
- Third Party F-Droid Repos
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
- Jenkins Jobs
|
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- Jenkins Jobs
|
||||||
- [Telegram App](https://telegram.org)
|
- Open Source - App-Specific:
|
||||||
- [Neutron Code](https://neutroncode.com)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- "HTML" (Fallback)
|
- [Signal](https://signal.org/)
|
||||||
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
- [VLC](https://videolan.org/)
|
||||||
|
- Other - App-Specific:
|
||||||
|
- [Telegram App](https://telegram.org)
|
||||||
|
- [Steam Mobile Apps](https://store.steampowered.com/mobile)
|
||||||
|
- [Neutron Code](https://neutroncode.com)
|
||||||
|
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"and": "e",
|
"and": "e",
|
||||||
"githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)",
|
"githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)",
|
||||||
"githubPATHint": "O TAP deve estar nesse formato: usuario:token",
|
|
||||||
"githubPATFormat": "usuario:token",
|
|
||||||
"includePrereleases": "Incluir pré-lançamentos",
|
"includePrereleases": "Incluir pré-lançamentos",
|
||||||
"fallbackToOlderReleases": "Retornar para versões anteriores",
|
"fallbackToOlderReleases": "Retornar para versões anteriores",
|
||||||
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
|
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
|
"searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
|
||||||
"search": "Procurar",
|
"search": "Procurar",
|
||||||
"additionalOptsFor": "Opções Adicionais para {}",
|
"additionalOptsFor": "Opções Adicionais para {}",
|
||||||
"supportedSourcesBelow": "Fontes Compatíveis:",
|
"supportedSources": "Fontes Compatíveis",
|
||||||
"trackOnlyInBrackets": "(Apenas Seguir)",
|
"trackOnlyInBrackets": "(Apenas Seguir)",
|
||||||
"searchableInBrackets": "(Pesquisável)",
|
"searchableInBrackets": "(Pesquisável)",
|
||||||
"appsString": "Apps",
|
"appsString": "Apps",
|
||||||
@ -252,6 +250,12 @@
|
|||||||
"intermediateLinkNotFound": "Link intermediário não encontrado",
|
"intermediateLinkNotFound": "Link intermediário não encontrado",
|
||||||
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
|
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
|
||||||
"bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
|
"bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remover App?",
|
"one": "Remover App?",
|
||||||
"other": "Remover Apps?"
|
"other": "Remover Apps?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Dobro",
|
"ok": "Dobro",
|
||||||
"and": "i",
|
"and": "i",
|
||||||
"githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)",
|
"githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)",
|
||||||
"githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token",
|
|
||||||
"githubPATFormat": "korisničko_ime:token",
|
|
||||||
"includePrereleases": "Uključi preliminarna izdanja",
|
"includePrereleases": "Uključi preliminarna izdanja",
|
||||||
"fallbackToOlderReleases": "Povratak na starija izdanja",
|
"fallbackToOlderReleases": "Povratak na starija izdanja",
|
||||||
"filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
|
"filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
|
"searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
|
||||||
"search": "Pretraživanje",
|
"search": "Pretraživanje",
|
||||||
"additionalOptsFor": "Dodatne opcije za {}",
|
"additionalOptsFor": "Dodatne opcije za {}",
|
||||||
"supportedSourcesBelow": "Podržani izvori:",
|
"supportedSources": "Podržani izvori",
|
||||||
"trackOnlyInBrackets": "(Samo za praćenje)",
|
"trackOnlyInBrackets": "(Samo za praćenje)",
|
||||||
"searchableInBrackets": "(Može se pretraživati)",
|
"searchableInBrackets": "(Može se pretraživati)",
|
||||||
"appsString": "Aplikacije",
|
"appsString": "Aplikacije",
|
||||||
@ -113,7 +111,7 @@
|
|||||||
"dark": "Tamna",
|
"dark": "Tamna",
|
||||||
"light": "Svijetla",
|
"light": "Svijetla",
|
||||||
"followSystem": "Pratite sistem",
|
"followSystem": "Pratite sistem",
|
||||||
"obtainium": "Obtainium",
|
"obtainium": "Obtainium",
|
||||||
"materialYou": "Material You",
|
"materialYou": "Material You",
|
||||||
"useBlackTheme": "Koristite čisto crnu tamnu temu",
|
"useBlackTheme": "Koristite čisto crnu tamnu temu",
|
||||||
"appSortBy": "Aplikacije sortirane po",
|
"appSortBy": "Aplikacije sortirane po",
|
||||||
@ -249,7 +247,13 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
"removeAppQuestion": {
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
|
"removeAppQuestion": {
|
||||||
"one": "Želite li ukloniti aplikaciju?",
|
"one": "Želite li ukloniti aplikaciju?",
|
||||||
"other": "Želite li ukloniti aplikacije?"
|
"other": "Želite li ukloniti aplikacije?"
|
||||||
},
|
},
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Okay",
|
"ok": "Okay",
|
||||||
"and": "und",
|
"and": "und",
|
||||||
"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",
|
|
||||||
"githubPATFormat": "Benutzername:Token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
|
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"additionalOptsFor": "Zusatzoptionen für {}",
|
"additionalOptsFor": "Zusatzoptionen für {}",
|
||||||
"supportedSourcesBelow": "Unterstützte Quellen:",
|
"supportedSources": "Unterstützte Quellen",
|
||||||
"trackOnlyInBrackets": "(Nur Nachverfolgen)",
|
"trackOnlyInBrackets": "(Nur Nachverfolgen)",
|
||||||
"searchableInBrackets": "(Durchsuchbar)",
|
"searchableInBrackets": "(Durchsuchbar)",
|
||||||
"appsString": "Apps",
|
"appsString": "Apps",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Überprüfe das 'latest' Tag",
|
"verifyLatestTag": "Überprüfe das 'latest' Tag",
|
||||||
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
|
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
|
||||||
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
|
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "App entfernen?",
|
"one": "App entfernen?",
|
||||||
"other": "Apps entfernen?"
|
"other": "Apps entfernen?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Okay",
|
"ok": "Okay",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"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",
|
|
||||||
"githubPATFormat": "username:token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Search (Some Sources Only)",
|
"searchSomeSourcesLabel": "Search (Some Sources Only)",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"additionalOptsFor": "Additional Options for {}",
|
"additionalOptsFor": "Additional Options for {}",
|
||||||
"supportedSourcesBelow": "Supported Sources:",
|
"supportedSources": "Supported Sources",
|
||||||
"trackOnlyInBrackets": "(Track-Only)",
|
"trackOnlyInBrackets": "(Track-Only)",
|
||||||
"searchableInBrackets": "(Searchable)",
|
"searchableInBrackets": "(Searchable)",
|
||||||
"appsString": "Apps",
|
"appsString": "Apps",
|
||||||
@ -252,6 +250,12 @@
|
|||||||
"intermediateLinkNotFound": "Intermediate link not found",
|
"intermediateLinkNotFound": "Intermediate link not found",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Remove App?",
|
"one": "Remove App?",
|
||||||
"other": "Remove Apps?"
|
"other": "Remove Apps?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Correcto",
|
"ok": "Correcto",
|
||||||
"and": "y",
|
"and": "y",
|
||||||
"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",
|
|
||||||
"githubPATFormat": "nombre_de_usuario:token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"additionalOptsFor": "Opciones Adicionales para {}",
|
"additionalOptsFor": "Opciones Adicionales para {}",
|
||||||
"supportedSourcesBelow": "Fuentes Soportadas:",
|
"supportedSources": "Fuentes Soportadas",
|
||||||
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
"trackOnlyInBrackets": "(Solo Seguimiento)",
|
||||||
"searchableInBrackets": "(Soporta Búsquedas)",
|
"searchableInBrackets": "(Soporta Búsquedas)",
|
||||||
"appsString": "Aplicaciones",
|
"appsString": "Aplicaciones",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "¿Eliminar Aplicación?",
|
"one": "¿Eliminar Aplicación?",
|
||||||
"other": "¿Eliminar Aplicaciones?"
|
"other": "¿Eliminar Aplicaciones?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "باشه",
|
"ok": "باشه",
|
||||||
"and": "و",
|
"and": "و",
|
||||||
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
||||||
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
|
||||||
"githubPATFormat": "username:token",
|
|
||||||
"includePrereleases": "شامل نسخه های اولیه",
|
"includePrereleases": "شامل نسخه های اولیه",
|
||||||
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
||||||
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
|
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
|
||||||
"search": "جستجو کردن",
|
"search": "جستجو کردن",
|
||||||
"additionalOptsFor": "گزینه های اضافی برای {}",
|
"additionalOptsFor": "گزینه های اضافی برای {}",
|
||||||
"supportedSourcesBelow": "منابع پشتیبانی شده:",
|
"supportedSources": "منابع پشتیبانی شده",
|
||||||
"trackOnlyInBrackets": "«فقط ردیابی»",
|
"trackOnlyInBrackets": "«فقط ردیابی»",
|
||||||
"searchableInBrackets": "(قابل جستجو)",
|
"searchableInBrackets": "(قابل جستجو)",
|
||||||
"appsString": "برنامه ها",
|
"appsString": "برنامه ها",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "برنامه حذف شود؟",
|
"one": "برنامه حذف شود؟",
|
||||||
"other": "برنامه ها حذف شوند؟"
|
"other": "برنامه ها حذف شوند؟"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Okay",
|
"ok": "Okay",
|
||||||
"and": "et",
|
"and": "et",
|
||||||
"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",
|
|
||||||
"githubPATFormat": "username:token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
|
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
"additionalOptsFor": "Options supplémentaires pour {}",
|
"additionalOptsFor": "Options supplémentaires pour {}",
|
||||||
"supportedSourcesBelow": "Sources prises en charge :",
|
"supportedSources": "Sources prises en charge ",
|
||||||
"trackOnlyInBrackets": "(Suivi uniquement)",
|
"trackOnlyInBrackets": "(Suivi uniquement)",
|
||||||
"searchableInBrackets": "(Recherchable)",
|
"searchableInBrackets": "(Recherchable)",
|
||||||
"appsString": "Applications",
|
"appsString": "Applications",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Supprimer l'application ?",
|
"one": "Supprimer l'application ?",
|
||||||
"other": "Supprimer les applications ?"
|
"other": "Supprimer les applications ?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Oké",
|
"ok": "Oké",
|
||||||
"and": "és",
|
"and": "és",
|
||||||
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
|
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
|
||||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
|
||||||
"githubPATFormat": "felhasználónév:token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Keresés (csak egyes források)",
|
"searchSomeSourcesLabel": "Keresés (csak egyes források)",
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
"additionalOptsFor": "További lehetőségek a következőhöz: {}",
|
"additionalOptsFor": "További lehetőségek a következőhöz: {}",
|
||||||
"supportedSourcesBelow": "Támogatott források:",
|
"supportedSources": "Támogatott források",
|
||||||
"trackOnlyInBrackets": "(Csak nyomonkövetés)",
|
"trackOnlyInBrackets": "(Csak nyomonkövetés)",
|
||||||
"searchableInBrackets": "(Kereshető)",
|
"searchableInBrackets": "(Kereshető)",
|
||||||
"appsString": "Appok",
|
"appsString": "Appok",
|
||||||
@ -248,6 +246,12 @@
|
|||||||
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
|
"verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
|
||||||
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
|
"exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
|
||||||
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
|
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"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?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Va bene",
|
"ok": "Va bene",
|
||||||
"and": "e",
|
"and": "e",
|
||||||
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
|
||||||
"githubPATHint": "PAT deve seguire questo formato: nomeutente:token",
|
|
||||||
"githubPATFormat": "nomeutente:token",
|
|
||||||
"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",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
|
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
"additionalOptsFor": "Opzioni aggiuntive per {}",
|
"additionalOptsFor": "Opzioni aggiuntive per {}",
|
||||||
"supportedSourcesBelow": "Fonti supportate:",
|
"supportedSources": "Fonti supportate",
|
||||||
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
|
||||||
"searchableInBrackets": "(ricercabile)",
|
"searchableInBrackets": "(ricercabile)",
|
||||||
"appsString": "App",
|
"appsString": "App",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Rimuovere l'app?",
|
"one": "Rimuovere l'app?",
|
||||||
"other": "Rimuovere le app?"
|
"other": "Rimuovere le app?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"and": "と",
|
"and": "と",
|
||||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
|
||||||
"githubPATFormat": "ユーザー名:トークン",
|
|
||||||
"includePrereleases": "プレリリースを含む",
|
"includePrereleases": "プレリリースを含む",
|
||||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
|
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
|
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"additionalOptsFor": "{}の追加オプション",
|
"additionalOptsFor": "{}の追加オプション",
|
||||||
"supportedSourcesBelow": "対応するソース:",
|
"supportedSources": "対応するソース",
|
||||||
"trackOnlyInBrackets": "(追跡のみ)",
|
"trackOnlyInBrackets": "(追跡のみ)",
|
||||||
"searchableInBrackets": "(検索可能)",
|
"searchableInBrackets": "(検索可能)",
|
||||||
"appsString": "アプリ",
|
"appsString": "アプリ",
|
||||||
@ -250,6 +248,12 @@
|
|||||||
"verifyLatestTag": "'latest'タグを確認する",
|
"verifyLatestTag": "'latest'タグを確認する",
|
||||||
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
|
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
|
||||||
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
|
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "アプリを削除しますか?",
|
"one": "アプリを削除しますか?",
|
||||||
"other": "アプリを削除しますか?"
|
"other": "アプリを削除しますか?"
|
||||||
|
@ -22,8 +22,6 @@
|
|||||||
"ok": "Okej",
|
"ok": "Okej",
|
||||||
"and": "i",
|
"and": "i",
|
||||||
"githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
|
"githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
|
||||||
"githubPATHint": "Wymagany format: użytkownik:token",
|
|
||||||
"githubPATFormat": "użytkownik:token",
|
|
||||||
"includePrereleases": "Uwzględnij wersje wstępne",
|
"includePrereleases": "Uwzględnij wersje wstępne",
|
||||||
"fallbackToOlderReleases": "Powracaj do starszych wersji",
|
"fallbackToOlderReleases": "Powracaj do starszych wersji",
|
||||||
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
|
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
|
||||||
@ -52,7 +50,7 @@
|
|||||||
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
|
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
"additionalOptsFor": "Dodatkowe opcje dla {}",
|
"additionalOptsFor": "Dodatkowe opcje dla {}",
|
||||||
"supportedSourcesBelow": "Obsługiwane źródła:",
|
"supportedSources": "Obsługiwane źródła",
|
||||||
"trackOnlyInBrackets": "(tylko obserwowane)",
|
"trackOnlyInBrackets": "(tylko obserwowane)",
|
||||||
"searchableInBrackets": "(Wyszukiwalne)",
|
"searchableInBrackets": "(Wyszukiwalne)",
|
||||||
"appsString": "Aplikacje",
|
"appsString": "Aplikacje",
|
||||||
@ -255,6 +253,12 @@
|
|||||||
"verifyLatestTag": "Zweryfikuj najnowszy tag",
|
"verifyLatestTag": "Zweryfikuj najnowszy tag",
|
||||||
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
|
"exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
|
||||||
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
|
"bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Usunąć aplikację?",
|
"one": "Usunąć aplikację?",
|
||||||
"few": "Usunąć aplikacje?",
|
"few": "Usunąć aplikacje?",
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "Окей",
|
"ok": "Окей",
|
||||||
"and": "и",
|
"and": "и",
|
||||||
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
|
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
|
||||||
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
|
|
||||||
"githubPATFormat": "имя_пользователя:токен",
|
|
||||||
"includePrereleases": "Включить предварительные релизы",
|
"includePrereleases": "Включить предварительные релизы",
|
||||||
"fallbackToOlderReleases": "Откатиться к более старым версиям",
|
"fallbackToOlderReleases": "Откатиться к более старым версиям",
|
||||||
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
|
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
|
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"additionalOptsFor": "Дополнительные опции для {}",
|
"additionalOptsFor": "Дополнительные опции для {}",
|
||||||
"supportedSourcesBelow": "Поддерживаемые источники:",
|
"supportedSources": "Поддерживаемые источники",
|
||||||
"trackOnlyInBrackets": "(Только для отслеживания)",
|
"trackOnlyInBrackets": "(Только для отслеживания)",
|
||||||
"searchableInBrackets": "(Поиск)",
|
"searchableInBrackets": "(Поиск)",
|
||||||
"appsString": "Приложения",
|
"appsString": "Приложения",
|
||||||
@ -249,6 +247,12 @@
|
|||||||
"verifyLatestTag": "Verify the 'latest' tag",
|
"verifyLatestTag": "Verify the 'latest' tag",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "Удалить приложение?",
|
"one": "Удалить приложение?",
|
||||||
"other": "Удалить приложения?"
|
"other": "Удалить приложения?"
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
"ok": "好的",
|
"ok": "好的",
|
||||||
"and": "和",
|
"and": "和",
|
||||||
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||||
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
|
||||||
"githubPATFormat": "username:token",
|
|
||||||
"includePrereleases": "包含预发行版",
|
"includePrereleases": "包含预发行版",
|
||||||
"fallbackToOlderReleases": "将旧发行版作为备选",
|
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||||
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
||||||
@ -43,7 +41,7 @@
|
|||||||
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
|
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"additionalOptsFor": "{} 的更多选项",
|
"additionalOptsFor": "{} 的更多选项",
|
||||||
"supportedSourcesBelow": "支持的来源:",
|
"supportedSources": "支持的来源",
|
||||||
"trackOnlyInBrackets": "(仅追踪)",
|
"trackOnlyInBrackets": "(仅追踪)",
|
||||||
"searchableInBrackets": "(可搜索)",
|
"searchableInBrackets": "(可搜索)",
|
||||||
"appsString": "应用列表",
|
"appsString": "应用列表",
|
||||||
@ -250,6 +248,12 @@
|
|||||||
"verifyLatestTag": "验证“Latest”标签",
|
"verifyLatestTag": "验证“Latest”标签",
|
||||||
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
|
||||||
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
|
||||||
|
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
|
||||||
|
"versionExtractionRegEx": "Version Extraction RegEx",
|
||||||
|
"matchGroupToUse": "Match Group to Use",
|
||||||
|
"highlightTouchTargets": "Highlight less obvious touch targets",
|
||||||
|
"pickExportDir": "Pick Export Directory",
|
||||||
|
"autoExportOnChanges": "Auto-export on changes",
|
||||||
"removeAppQuestion": {
|
"removeAppQuestion": {
|
||||||
"one": "是否删除应用?",
|
"one": "是否删除应用?",
|
||||||
"other": "是否删除应用?"
|
"other": "是否删除应用?"
|
||||||
|
4
build.sh
4
build.sh
@ -4,7 +4,9 @@
|
|||||||
CURR_DIR="$(pwd)"
|
CURR_DIR="$(pwd)"
|
||||||
trap "cd "$CURR_DIR"" EXIT
|
trap "cd "$CURR_DIR"" EXIT
|
||||||
|
|
||||||
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
|
if [ -z "$1" ]; then
|
||||||
|
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
|
||||||
|
fi
|
||||||
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
|
rm ./build/app/outputs/flutter-apk/* 2>/dev/null # Get rid of older builds if any
|
||||||
flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs)
|
flutter build apk && flutter build apk --split-per-abi # Build (both split and combined APKs)
|
||||||
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures
|
for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures
|
||||||
|
@ -25,12 +25,16 @@ class APKCombo extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, String> get requestHeaders => {
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
"User-Agent": "curl/8.0.1",
|
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||||
"Accept": "*/*",
|
bool forAPKDownload = false}) async {
|
||||||
"Connection": "keep-alive",
|
return {
|
||||||
"Host": "$host"
|
"User-Agent": "curl/8.0.1",
|
||||||
};
|
"Accept": "*/*",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Host": "$host"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
|
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
|
||||||
var res = await sourceRequest('$standardUrl/download/apk');
|
var res = await sourceRequest('$standardUrl/download/apk');
|
||||||
|
@ -3,6 +3,21 @@ import 'package:html/parser.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';
|
||||||
|
|
||||||
|
parseDateTimeMMMddCommayyyy(String? dateString) {
|
||||||
|
DateTime? releaseDate;
|
||||||
|
try {
|
||||||
|
releaseDate = dateString != null
|
||||||
|
? DateFormat('MMM dd, yyyy').parse(dateString)
|
||||||
|
: null;
|
||||||
|
releaseDate = dateString != null && releaseDate == null
|
||||||
|
? DateFormat('MMMM dd, yyyy').parse(dateString)
|
||||||
|
: releaseDate;
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return releaseDate;
|
||||||
|
}
|
||||||
|
|
||||||
class APKPure extends AppSource {
|
class APKPure extends AppSource {
|
||||||
APKPure() {
|
APKPure() {
|
||||||
host = 'apkpure.com';
|
host = 'apkpure.com';
|
||||||
@ -47,17 +62,7 @@ class APKPure extends AppSource {
|
|||||||
}
|
}
|
||||||
String? dateString =
|
String? dateString =
|
||||||
html.querySelector('span.info-other span.date')?.text.trim();
|
html.querySelector('span.info-other span.date')?.text.trim();
|
||||||
DateTime? releaseDate;
|
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
|
||||||
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';
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
List<MapEntry<String, String>> apkUrls = [
|
List<MapEntry<String, String>> apkUrls = [
|
||||||
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||||
@ -70,11 +75,13 @@ class APKPure extends AppSource {
|
|||||||
Uri.parse(standardUrl).pathSegments.reversed.last;
|
Uri.parse(standardUrl).pathSegments.reversed.last;
|
||||||
String appName =
|
String appName =
|
||||||
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
||||||
String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml
|
String? changeLog = htmlChangelog
|
||||||
.trim().replaceAll("<br>", " \n");
|
.querySelector("div.whats-new-info p:not(.date)")
|
||||||
|
?.innerHtml
|
||||||
|
.trim()
|
||||||
|
.replaceAll("<br>", " \n");
|
||||||
return APKDetails(version, apkUrls, AppNames(author, appName),
|
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate, changeLog: changeLog);
|
||||||
changeLog: changeLog);
|
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -26,14 +24,10 @@ class Aptoide extends AppSource {
|
|||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
return (await getLatestAPKDetails(standardUrl, additionalSettings)).version;
|
return (await getAppDetailsJSON(standardUrl))['package'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async {
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
|
||||||
String standardUrl,
|
|
||||||
Map<String, dynamic> additionalSettings,
|
|
||||||
) async {
|
|
||||||
var res = await sourceRequest(standardUrl);
|
var res = await sourceRequest(standardUrl);
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
@ -50,12 +44,20 @@ class Aptoide extends AppSource {
|
|||||||
if (res2.statusCode != 200) {
|
if (res2.statusCode != 200) {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
var appDetails = jsonDecode(res2.body)?['nodes']?['meta']?['data'];
|
return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
|
||||||
String appName = appDetails?['name'] ?? tr('app');
|
}
|
||||||
String author = appDetails?['developer']?['name'] ?? name;
|
|
||||||
String? dateStr = appDetails?['updated'];
|
@override
|
||||||
String? version = appDetails?['file']?['vername'];
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String? apkUrl = appDetails?['file']?['path'];
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
var appDetails = await getAppDetailsJSON(standardUrl);
|
||||||
|
String appName = appDetails['name'] ?? tr('app');
|
||||||
|
String author = appDetails['developer']?['name'] ?? name;
|
||||||
|
String? dateStr = appDetails['updated'];
|
||||||
|
String? version = appDetails['file']?['vername'];
|
||||||
|
String? apkUrl = appDetails['file']?['path'];
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
@ -71,34 +73,4 @@ class Aptoide extends AppSource {
|
|||||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||||
releaseDate: relDate);
|
releaseDate: relDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Map<String, List<String>>> search(String query,
|
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
|
||||||
Response res = await sourceRequest(
|
|
||||||
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
|
||||||
String? url = e.attributes['href'];
|
|
||||||
if (url != null) {
|
|
||||||
try {
|
|
||||||
standardizeUrl(url);
|
|
||||||
} catch (e) {
|
|
||||||
url = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (url != null) {
|
|
||||||
urlsWithDescriptions[url] = [
|
|
||||||
e.querySelector('.package-name')?.text.trim() ?? '',
|
|
||||||
e.querySelector('.package-summary')?.text.trim() ??
|
|
||||||
tr('noDescription')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return urlsWithDescriptions;
|
|
||||||
} else {
|
|
||||||
throw getObtainiumHttpError(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -11,6 +12,12 @@ class FDroid extends AppSource {
|
|||||||
host = 'f-droid.org';
|
host = 'f-droid.org';
|
||||||
name = tr('fdroid');
|
name = tr('fdroid');
|
||||||
canSearch = true;
|
canSearch = true;
|
||||||
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch('autoSelectHighestVersionCode',
|
||||||
|
label: tr('autoSelectHighestVersionCode'))
|
||||||
|
]
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -37,7 +44,8 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
Response res, String apkUrlPrefix, String standardUrl) {
|
Response res, String apkUrlPrefix, String standardUrl,
|
||||||
|
{bool autoSelectHighestVersionCode = false}) {
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
|
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
|
||||||
if (releases.isEmpty) {
|
if (releases.isEmpty) {
|
||||||
@ -47,8 +55,12 @@ class FDroid extends AppSource {
|
|||||||
if (latestVersion == null) {
|
if (latestVersion == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
List<String> apkUrls = releases
|
Iterable<dynamic> latestReleases =
|
||||||
.where((element) => element['versionName'] == latestVersion)
|
releases.where((element) => element['versionName'] == latestVersion);
|
||||||
|
if (latestReleases.length > 1 && autoSelectHighestVersionCode) {
|
||||||
|
latestReleases = [latestReleases.first];
|
||||||
|
}
|
||||||
|
List<String> apkUrls = latestReleases
|
||||||
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.toList();
|
.toList();
|
||||||
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
|
||||||
@ -68,7 +80,9 @@ class FDroid extends AppSource {
|
|||||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||||
'https://$host/repo/$appId',
|
'https://$host/repo/$appId',
|
||||||
standardUrl);
|
standardUrl,
|
||||||
|
autoSelectHighestVersionCode:
|
||||||
|
additionalSettings['autoSelectHighestVersionCode'] == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
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';
|
||||||
@ -21,20 +22,6 @@ class GitHub extends AppSource {
|
|||||||
label: tr('githubPATLabel'),
|
label: tr('githubPATLabel'),
|
||||||
password: true,
|
password: true,
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
|
||||||
(value) {
|
|
||||||
if (value != null && value.trim().isNotEmpty) {
|
|
||||||
if (value
|
|
||||||
.split(':')
|
|
||||||
.where((element) => element.trim().isNotEmpty)
|
|
||||||
.length !=
|
|
||||||
2) {
|
|
||||||
return tr('githubPATHint');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hint: tr('githubPATFormat'),
|
hint: tr('githubPATFormat'),
|
||||||
belowWidgets: [
|
belowWidgets: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -169,26 +156,53 @@ class GitHub extends AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getCredentialPrefixIfAny(
|
@override
|
||||||
Map<String, dynamic> additionalSettings) async {
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
|
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||||
|
bool forAPKDownload = false}) async {
|
||||||
|
var token = await getTokenIfAny(additionalSettings);
|
||||||
|
var headers = <String, String>{};
|
||||||
|
if (token != null) {
|
||||||
|
headers[HttpHeaders.authorizationHeader] = 'Token $token';
|
||||||
|
}
|
||||||
|
if (forAPKDownload == true) {
|
||||||
|
headers[HttpHeaders.acceptHeader] = 'application/octet-stream';
|
||||||
|
}
|
||||||
|
if (headers.isNotEmpty) {
|
||||||
|
return headers;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
|
||||||
SettingsProvider settingsProvider = SettingsProvider();
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
await settingsProvider.initializeSettings();
|
await settingsProvider.initializeSettings();
|
||||||
var sourceConfig =
|
var sourceConfig =
|
||||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
await getSourceConfigValues(additionalSettings, settingsProvider);
|
||||||
String? creds = sourceConfig['github-creds'];
|
String? creds = sourceConfig['github-creds'];
|
||||||
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
if (creds != null) {
|
||||||
|
var userNameEndIndex = creds.indexOf(':');
|
||||||
|
if (userNameEndIndex > 0) {
|
||||||
|
creds = creds.substring(
|
||||||
|
userNameEndIndex + 1); // For old username-included token inputs
|
||||||
|
}
|
||||||
|
return creds;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> getSourceNote() async {
|
Future<String?> getSourceNote() async {
|
||||||
if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) {
|
if (!hostChanged && (await getTokenIfAny({})) == null) {
|
||||||
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
||||||
'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host';
|
'https://api.$host';
|
||||||
|
|
||||||
Future<String> convertStandardUrlToAPIUrl(
|
Future<String> convertStandardUrlToAPIUrl(
|
||||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
||||||
@ -238,9 +252,10 @@ class GitHub extends AppSource {
|
|||||||
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||||
(release['assets'] as List<dynamic>?)
|
(release['assets'] as List<dynamic>?)
|
||||||
?.map((e) {
|
?.map((e) {
|
||||||
return e['name'] != null && e['browser_download_url'] != null
|
return (e['name'] != null) &&
|
||||||
|
((e['url'] ?? e['browser_download_url']) != null)
|
||||||
? MapEntry(e['name'] as String,
|
? MapEntry(e['name'] as String,
|
||||||
e['browser_download_url'] as String)
|
(e['url'] ?? e['browser_download_url']) as String)
|
||||||
: const MapEntry('', '');
|
: const MapEntry('', '');
|
||||||
})
|
})
|
||||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
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/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
@ -18,7 +19,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
|||||||
.toList();
|
.toList();
|
||||||
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||||
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
|
||||||
} else if (ambiguousUrl.split('/').length == 1) {
|
} else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
|
||||||
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
|
||||||
} else {
|
} else {
|
||||||
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
|
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
|
||||||
@ -109,6 +110,23 @@ class HTML extends AppSource {
|
|||||||
hint: '([0-9]+\.)*[0-9]+/\$',
|
hint: '([0-9]+\.)*[0-9]+/\$',
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [(value) => regExValidator(value)])
|
additionalValidators: [(value) => regExValidator(value)])
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormTextField('versionExtractionRegEx',
|
||||||
|
label: tr('versionExtractionRegEx'),
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [(value) => regExValidator(value)]),
|
||||||
|
GeneratedFormTextField('matchGroupToUse',
|
||||||
|
label: tr('matchGroupToUse'),
|
||||||
|
required: false,
|
||||||
|
hint: '1',
|
||||||
|
textInputType: const TextInputType.numberWithOptions(),
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
value ??= '1';
|
||||||
|
return intValidator(value);
|
||||||
|
}
|
||||||
|
])
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
overrideVersionDetectionFormDefault('noVersionDetection',
|
overrideVersionDetectionFormDefault('noVersionDetection',
|
||||||
@ -116,11 +134,14 @@ class HTML extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// TODO: implement requestHeaders choice, hardcoded for now
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
Map<String, String>? get requestHeaders => {
|
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||||
"User-Agent":
|
bool forAPKDownload = false}) async {
|
||||||
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
|
return {
|
||||||
};
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
@ -180,10 +201,23 @@ class HTML extends AppSource {
|
|||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
var rel = links.last;
|
var rel = links.last;
|
||||||
var version = rel.hashCode.toString();
|
String? version = rel.hashCode.toString();
|
||||||
|
var versionExtractionRegEx =
|
||||||
|
additionalSettings['versionExtractionRegEx'] as String?;
|
||||||
|
if (versionExtractionRegEx?.isNotEmpty == true) {
|
||||||
|
var match = RegExp(versionExtractionRegEx!).allMatches(rel);
|
||||||
|
if (match.isEmpty) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
version = match.last
|
||||||
|
.group(int.parse(additionalSettings['matchGroupToUse'] as String));
|
||||||
|
if (version?.isEmpty == true) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
List<String> apkUrls =
|
List<String> apkUrls =
|
||||||
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
|
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
|
||||||
return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(),
|
return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(),
|
||||||
AppNames(uri.host, tr('app')));
|
AppNames(uri.host, tr('app')));
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
|
@ -3,8 +3,13 @@ import 'package:obtainium/custom_errors.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class IzzyOnDroid extends AppSource {
|
class IzzyOnDroid extends AppSource {
|
||||||
|
late FDroid fd;
|
||||||
|
|
||||||
IzzyOnDroid() {
|
IzzyOnDroid() {
|
||||||
host = 'android.izzysoft.de';
|
host = 'android.izzysoft.de';
|
||||||
|
fd = FDroid();
|
||||||
|
additionalSourceAppSpecificSettingFormItems =
|
||||||
|
fd.additionalSourceAppSpecificSettingFormItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -20,7 +25,7 @@ class IzzyOnDroid extends AppSource {
|
|||||||
@override
|
@override
|
||||||
Future<String?> tryInferringAppId(String standardUrl,
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
return FDroid().tryInferringAppId(standardUrl);
|
return fd.tryInferringAppId(standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,10 +34,12 @@ class IzzyOnDroid extends AppSource {
|
|||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
String? appId = await tryInferringAppId(standardUrl);
|
String? appId = await tryInferringAppId(standardUrl);
|
||||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
await sourceRequest(
|
await sourceRequest(
|
||||||
'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,
|
||||||
|
autoSelectHighestVersionCode:
|
||||||
|
additionalSettings['autoSelectHighestVersionCode'] == true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
82
lib/app_sources/uptodown.dart
Normal file
82
lib/app_sources/uptodown.dart
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:obtainium/app_sources/apkpure.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class Uptodown extends AppSource {
|
||||||
|
Uptodown() {
|
||||||
|
host = 'uptodown.com';
|
||||||
|
allowSubDomains = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return '${url.substring(0, match.end)}/android/download';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
|
return (await getAppDetailsFromPage(standardUrl))['appId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async {
|
||||||
|
var res = await sourceRequest(standardUrl);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var html = parse(res.body);
|
||||||
|
String? version = html.querySelector('div.version')?.innerHtml;
|
||||||
|
String? apkUrl =
|
||||||
|
html.querySelector('#detail-download-button')?.attributes['data-url'];
|
||||||
|
String? name = html.querySelector('#detail-app-name')?.innerHtml.trim();
|
||||||
|
String? author = html.querySelector('#author-link')?.innerHtml.trim();
|
||||||
|
var detailElements = html.querySelectorAll('#technical-information td');
|
||||||
|
String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim();
|
||||||
|
String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim();
|
||||||
|
return Map.fromEntries([
|
||||||
|
MapEntry('version', version),
|
||||||
|
MapEntry('apkUrl', apkUrl),
|
||||||
|
MapEntry('appId', appId),
|
||||||
|
MapEntry('name', name),
|
||||||
|
MapEntry('author', author),
|
||||||
|
MapEntry('dateStr', dateStr)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
var appDetails = await getAppDetailsFromPage(standardUrl);
|
||||||
|
var version = appDetails['version'];
|
||||||
|
var apkUrl = appDetails['apkUrl'];
|
||||||
|
var appId = appDetails['appId'];
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
if (apkUrl == null) {
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
if (appId == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
String appName = appDetails['name'] ?? tr('app');
|
||||||
|
String author = appDetails['author'] ?? name;
|
||||||
|
String? dateStr = appDetails['dateStr'];
|
||||||
|
DateTime? relDate;
|
||||||
|
if (dateStr != null) {
|
||||||
|
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||||
|
releaseDate: relDate);
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,12 @@ class VLC extends AppSource {
|
|||||||
get dwUrlBase => 'https://get.$host/vlc-android/';
|
get dwUrlBase => 'https://get.$host/vlc-android/';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, String>? get requestHeaders => HTML().requestHeaders;
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
|
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||||
|
bool forAPKDownload = false}) =>
|
||||||
|
HTML().getRequestHeaders(
|
||||||
|
additionalSettings: additionalSettings,
|
||||||
|
forAPKDownload: forAPKDownload);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String sourceSpecificStandardizeURL(String url) {
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
late int max;
|
late int max;
|
||||||
late String? hint;
|
late String? hint;
|
||||||
late bool password;
|
late bool password;
|
||||||
|
late TextInputType? textInputType;
|
||||||
|
|
||||||
GeneratedFormTextField(String key,
|
GeneratedFormTextField(String key,
|
||||||
{String label = 'Input',
|
{String label = 'Input',
|
||||||
@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.hint,
|
this.hint,
|
||||||
this.password = false})
|
this.password = false,
|
||||||
|
this.textInputType})
|
||||||
: super(key,
|
: super(key,
|
||||||
label: label,
|
label: label,
|
||||||
belowWidgets: belowWidgets,
|
belowWidgets: belowWidgets,
|
||||||
@ -144,7 +146,8 @@ Color generateRandomLightColor() {
|
|||||||
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
|
||||||
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
|
||||||
// Map RBG values from 0-1 to 0-255:
|
// Map RBG values from 0-1 to 0-255:
|
||||||
final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
|
final List<int> rgbValues =
|
||||||
|
rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
|
||||||
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
if (formItem is GeneratedFormTextField) {
|
if (formItem is GeneratedFormTextField) {
|
||||||
final formFieldKey = GlobalKey<FormFieldState>();
|
final formFieldKey = GlobalKey<FormFieldState>();
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
|
keyboardType: formItem.textInputType,
|
||||||
obscureText: formItem.password,
|
obscureText: formItem.password,
|
||||||
autocorrect: !formItem.password,
|
autocorrect: !formItem.password,
|
||||||
enableSuggestions: !formItem.password,
|
enableSuggestions: !formItem.password,
|
||||||
@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
}) ??
|
}) ??
|
||||||
[const SizedBox.shrink()],
|
[const SizedBox.shrink()],
|
||||||
(values[widget.items[r][e].key]
|
(values[widget.items[r][e].key]
|
||||||
as Map<String, MapEntry<int, bool>>?)
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
?.values
|
?.values
|
||||||
.where((e) => e.value)
|
.where((e) => e.value)
|
||||||
.length == 1
|
.length ==
|
||||||
|
1
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
var temp = values[widget.items[r][e].key]
|
var temp = values[widget.items[r][e].key]
|
||||||
as Map<String, MapEntry<int, bool>>;
|
as Map<String, MapEntry<int, bool>>;
|
||||||
// get selected category str where bool is true
|
// get selected category str where bool is true
|
||||||
final oldEntry = temp.entries.firstWhere((entry) => entry.value.value);
|
final oldEntry = temp.entries
|
||||||
// generate new color, ensure it is not the same
|
.firstWhere((entry) => entry.value.value);
|
||||||
int newColor = oldEntry.value.key;
|
// generate new color, ensure it is not the same
|
||||||
while(oldEntry.value.key == newColor) {
|
int newColor = oldEntry.value.key;
|
||||||
newColor = generateRandomLightColor().value;
|
while (oldEntry.value.key == newColor) {
|
||||||
}
|
newColor = generateRandomLightColor().value;
|
||||||
// Update entry with new color, remain selected
|
}
|
||||||
temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value));
|
// Update entry with new color, remain selected
|
||||||
values[widget.items[r][e].key] = temp;
|
temp.update(oldEntry.key,
|
||||||
someValueChanged();
|
(old) => MapEntry(newColor, old.value));
|
||||||
});
|
values[widget.items[r][e].key] = temp;
|
||||||
},
|
someValueChanged();
|
||||||
icon: const Icon(Icons.format_color_fill_rounded),
|
});
|
||||||
visualDensity: VisualDensity.compact,
|
},
|
||||||
tooltip: tr('colour'),
|
icon: const Icon(Icons.format_color_fill_rounded),
|
||||||
))
|
visualDensity: VisualDensity.compact,
|
||||||
|
tooltip: tr('colour'),
|
||||||
|
))
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
(values[widget.items[r][e].key]
|
(values[widget.items[r][e].key]
|
||||||
as Map<String, MapEntry<int, bool>>?)
|
as Map<String, MapEntry<int, bool>>?)
|
||||||
|
@ -19,7 +19,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.14.7';
|
const String currentVersion = '0.14.13';
|
||||||
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
|
||||||
|
|
||||||
|
@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
|
|
||||||
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username, int page) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(
|
||||||
'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
Uri.parse(
|
||||||
|
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
|
||||||
|
headers: await GitHub().getRequestHeaders());
|
||||||
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) as List<dynamic>)) {
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
|
@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart';
|
|||||||
import 'package:obtainium/pages/import_export.dart';
|
import 'package:obtainium/pages/import_export.dart';
|
||||||
import 'package:obtainium/pages/settings.dart';
|
import 'package:obtainium/pages/settings.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
NotificationsProvider notificationsProvider =
|
||||||
|
context.read<NotificationsProvider>();
|
||||||
|
|
||||||
bool doingSomething = gettingAppInfo || searching;
|
bool doingSomething = gettingAppInfo || searching;
|
||||||
|
|
||||||
@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
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 downloadedArtifact = await appsProvider.downloadApp(
|
var downloadedArtifact = await appsProvider.downloadApp(
|
||||||
app, globalNavigatorKey.currentContext);
|
app, globalNavigatorKey.currentContext,
|
||||||
|
notificationsProvider: notificationsProvider);
|
||||||
DownloadedApk? downloadedFile;
|
DownloadedApk? downloadedFile;
|
||||||
DownloadedXApkDir? downloadedDir;
|
DownloadedXApkDir? downloadedDir;
|
||||||
if (downloadedArtifact is DownloadedApk) {
|
if (downloadedArtifact is DownloadedApk) {
|
||||||
@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
tr('supportedSourcesBelow'),
|
tr('supportedSources'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 16,
|
||||||
),
|
),
|
||||||
...sourceProvider.sources
|
...sourceProvider.sources
|
||||||
.map((e) => GestureDetector(
|
.map((e) => GestureDetector(
|
||||||
@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
: const SizedBox();
|
: const SizedBox();
|
||||||
},
|
},
|
||||||
future: pickedSource?.getSourceNote()),
|
future: pickedSource?.getSourceNote()),
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
height: 16,
|
height: pickedSource != null ? 16 : 96,
|
||||||
),
|
),
|
||||||
if (pickedSource != null)
|
if (pickedSource != null) getAdditionalOptsCol(),
|
||||||
getAdditionalOptsCol()
|
if (pickedSource == null)
|
||||||
else
|
const Divider(
|
||||||
getSourcesListWidget(),
|
height: 48,
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
if (pickedSource == null) getSourcesListWidget(),
|
||||||
|
SizedBox(
|
||||||
|
height: pickedSource != null ? 8 : 2,
|
||||||
),
|
),
|
||||||
])),
|
])),
|
||||||
)
|
)
|
||||||
|
@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
try {
|
try {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||||
app?.app.id != null ? [app!.app.id] : [],
|
app?.app.id != null ? [app!.app.id] : [],
|
||||||
globalNavigatorKey.currentContext,
|
globalNavigatorKey.currentContext,
|
||||||
settingsProvider);
|
);
|
||||||
if (app?.app.installedVersion != null && !trackOnly) {
|
if (app?.app.installedVersion != null && !trackOnly) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
showError(tr('appsUpdated'), context);
|
showError(tr('appsUpdated'), context);
|
||||||
|
@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: () {
|
: () {
|
||||||
appsProvider.downloadAndInstallLatestApps(
|
appsProvider.downloadAndInstallLatestApps(
|
||||||
[listedApps[appIndex].app.id],
|
[listedApps[appIndex].app.id],
|
||||||
globalNavigatorKey.currentContext,
|
globalNavigatorKey.currentContext).catchError((e) {
|
||||||
settingsProvider).catchError((e) {
|
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
return <String>[];
|
return <String>[];
|
||||||
});
|
});
|
||||||
@ -449,33 +448,48 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: showChangesFn,
|
onTap: showChangesFn,
|
||||||
child: Column(
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
decoration: BoxDecoration(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
borderRadius: BorderRadius.circular(12),
|
||||||
children: [
|
color: settingsProvider.highlightTouchTargets &&
|
||||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
showChangesFn != null
|
||||||
Container(
|
? (Theme.of(context).brightness == Brightness.light
|
||||||
constraints: BoxConstraints(
|
? Theme.of(context).primaryColor
|
||||||
maxWidth: MediaQuery.of(context).size.width / 4),
|
: Theme.of(context).primaryColorLight)
|
||||||
child: Text(getVersionText(index),
|
.withAlpha(20)
|
||||||
overflow: TextOverflow.ellipsis,
|
: null),
|
||||||
textAlign: TextAlign.end)),
|
padding: settingsProvider.highlightTouchTargets
|
||||||
]),
|
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
|
||||||
Row(
|
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
getChangesButtonString(index, showChangesFn != null),
|
Container(
|
||||||
style: TextStyle(
|
constraints: BoxConstraints(
|
||||||
fontStyle: FontStyle.italic,
|
maxWidth:
|
||||||
decoration: showChangesFn != null
|
MediaQuery.of(context).size.width / 4),
|
||||||
? TextDecoration.underline
|
child: Text(getVersionText(index),
|
||||||
: TextDecoration.none),
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
textAlign: TextAlign.end)),
|
||||||
|
]),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getChangesButtonString(
|
||||||
|
index, showChangesFn != null),
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
decoration: showChangesFn != null
|
||||||
|
? TextDecoration.underline
|
||||||
|
: TextDecoration.none),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
)))
|
||||||
],
|
|
||||||
))
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -684,8 +698,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
|
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
|
||||||
}
|
}
|
||||||
appsProvider
|
appsProvider
|
||||||
.downloadAndInstallLatestApps(toInstall,
|
.downloadAndInstallLatestApps(
|
||||||
globalNavigatorKey.currentContext, settingsProvider)
|
toInstall, globalNavigatorKey.currentContext)
|
||||||
.catchError((e) {
|
.catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
return <String>[];
|
return <String>[];
|
||||||
|
@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.read<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
|
||||||
var outlineButtonStyle = ButtonStyle(
|
var outlineButtonStyle = ButtonStyle(
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runObtainiumExport() {
|
runObtainiumExport() async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
appsProvider.exportApps().then((String path) {
|
appsProvider
|
||||||
showError(tr('exportedTo', args: [path]), context);
|
.exportApps(
|
||||||
|
pickOnly: (await settingsProvider.getExportDir()) == null,
|
||||||
|
sp: settingsProvider)
|
||||||
|
.then((String? result) {
|
||||||
|
if (result != null) {
|
||||||
|
showError(tr('exportedTo', args: [result]), context);
|
||||||
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
showError(e, context);
|
showError(e, context);
|
||||||
});
|
});
|
||||||
@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
FutureBuilder(
|
||||||
children: [
|
future: settingsProvider.getExportDir(),
|
||||||
Expanded(
|
builder: (context, snapshot) {
|
||||||
child: TextButton(
|
return Column(
|
||||||
style: outlineButtonStyle,
|
children: [
|
||||||
onPressed: appsProvider.apps.isEmpty ||
|
Row(
|
||||||
importInProgress
|
children: [
|
||||||
? null
|
Expanded(
|
||||||
: runObtainiumExport,
|
child: TextButton(
|
||||||
child: Text(tr('obtainiumExport')))),
|
style: outlineButtonStyle,
|
||||||
const SizedBox(
|
onPressed: appsProvider.apps.isEmpty ||
|
||||||
width: 16,
|
importInProgress
|
||||||
),
|
? null
|
||||||
Expanded(
|
: runObtainiumExport,
|
||||||
child: TextButton(
|
child: Text(tr(snapshot.data != null
|
||||||
style: outlineButtonStyle,
|
? 'obtainiumExport'
|
||||||
onPressed: importInProgress
|
: 'pickExportDir')),
|
||||||
? null
|
)),
|
||||||
: runObtainiumImport,
|
const SizedBox(
|
||||||
child: Text(tr('obtainiumImport'))))
|
width: 16,
|
||||||
],
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextButton(
|
||||||
|
style: outlineButtonStyle,
|
||||||
|
onPressed: importInProgress
|
||||||
|
? null
|
||||||
|
: runObtainiumImport,
|
||||||
|
child: Text(tr('obtainiumImport'))))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (snapshot.data != null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormSwitch(
|
||||||
|
'autoExportOnChanges',
|
||||||
|
label: tr('autoExportOnChanges'),
|
||||||
|
defaultValue: settingsProvider
|
||||||
|
.autoExportOnChanges,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges:
|
||||||
|
(value, valid, isBuilding) {
|
||||||
|
if (valid && !isBuilding) {
|
||||||
|
if (value['autoExportOnChanges'] !=
|
||||||
|
null) {
|
||||||
|
settingsProvider
|
||||||
|
.autoExportOnChanges = value[
|
||||||
|
'autoExportOnChanges'] ==
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (importInProgress)
|
if (importInProgress)
|
||||||
const Column(
|
const Column(
|
||||||
@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
fontStyle: FontStyle.italic, fontSize: 12)),
|
fontStyle: FontStyle.italic, fontSize: 12)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)))
|
)))
|
||||||
]));
|
]));
|
||||||
|
@ -484,6 +484,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
height16,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(tr('highlightTouchTargets'))),
|
||||||
|
Switch(
|
||||||
|
value:
|
||||||
|
settingsProvider.highlightTouchTargets,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.highlightTouchTargets =
|
||||||
|
value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
height32,
|
height32,
|
||||||
Text(
|
Text(
|
||||||
tr('categories'),
|
tr('categories'),
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||||
import 'package:android_intent_plus/flag.dart';
|
import 'package:android_intent_plus/flag.dart';
|
||||||
@ -30,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart';
|
|||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:flutter_archive/flutter_archive.dart';
|
import 'package:flutter_archive/flutter_archive.dart';
|
||||||
|
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||||
|
|
||||||
final pm = AndroidPackageManager();
|
final pm = AndroidPackageManager();
|
||||||
|
|
||||||
@ -149,6 +151,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
late Stream<FGBGType>? foregroundStream;
|
late Stream<FGBGType>? foregroundStream;
|
||||||
late StreamSubscription<FGBGType>? foregroundSubscription;
|
late StreamSubscription<FGBGType>? foregroundSubscription;
|
||||||
late Directory APKDir;
|
late Directory APKDir;
|
||||||
|
late SettingsProvider settingsProvider = SettingsProvider();
|
||||||
|
|
||||||
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
|
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
|
||||||
|
|
||||||
@ -160,6 +163,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (isForeground) await loadApps();
|
if (isForeground) await loadApps();
|
||||||
});
|
});
|
||||||
() async {
|
() async {
|
||||||
|
await settingsProvider.initializeSettings();
|
||||||
var cacheDirs = await getExternalCacheDirectories();
|
var cacheDirs = await getExternalCacheDirectories();
|
||||||
if (cacheDirs?.isNotEmpty ?? false) {
|
if (cacheDirs?.isNotEmpty ?? false) {
|
||||||
APKDir = cacheDirs!.first;
|
APKDir = cacheDirs!.first;
|
||||||
@ -215,7 +219,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (headers != null) {
|
if (headers != null) {
|
||||||
req.headers.addAll(headers);
|
req.headers.addAll(headers);
|
||||||
}
|
}
|
||||||
var client = Client();
|
var client = http.Client();
|
||||||
StreamedResponse response = await client.send(req);
|
StreamedResponse response = await client.send(req);
|
||||||
String ext =
|
String ext =
|
||||||
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
response.headers['content-disposition']?.split('.').last ?? 'apk';
|
||||||
@ -298,9 +302,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
notificationsProvider?.cancel(notif.id);
|
notificationsProvider?.cancel(notif.id);
|
||||||
int? prevProg;
|
int? prevProg;
|
||||||
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
|
||||||
|
var headers = await source.getRequestHeaders(
|
||||||
|
additionalSettings: app.additionalSettings, forAPKDownload: true);
|
||||||
var downloadedFile = await downloadFileWithRetry(
|
var downloadedFile = await downloadFileWithRetry(
|
||||||
downloadUrl, fileNameNoExt, headers: source.requestHeaders,
|
downloadUrl, fileNameNoExt,
|
||||||
(double? progress) {
|
headers: headers, (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;
|
||||||
@ -366,8 +372,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.where((element) => element.downloadProgress != null)
|
.where((element) => element.downloadProgress != null)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
Future<bool> canInstallSilently(
|
Future<bool> canInstallSilently(App app) async {
|
||||||
App app, SettingsProvider settingsProvider) async {
|
|
||||||
if (app.id == obtainiumId) {
|
if (app.id == obtainiumId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -536,7 +541,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
getHost(apkUrl.value) != getHost(app.url) &&
|
getHost(apkUrl.value) != getHost(app.url) &&
|
||||||
context != null) {
|
context != null) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
var settingsProvider = context.read<SettingsProvider>();
|
|
||||||
if (!(settingsProvider.hideAPKOriginWarning) &&
|
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
await showDialog(
|
await showDialog(
|
||||||
@ -557,8 +561,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// If no BuildContext is provided, apps that require user interaction are ignored
|
// If no BuildContext is provided, apps that require user interaction are ignored
|
||||||
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
|
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
|
||||||
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
||||||
Future<List<String>> downloadAndInstallLatestApps(List<String> appIds,
|
Future<List<String>> downloadAndInstallLatestApps(
|
||||||
BuildContext? context, SettingsProvider settingsProvider,
|
List<String> appIds, BuildContext? context,
|
||||||
{NotificationsProvider? notificationsProvider}) async {
|
{NotificationsProvider? notificationsProvider}) async {
|
||||||
notificationsProvider =
|
notificationsProvider =
|
||||||
notificationsProvider ?? context?.read<NotificationsProvider>();
|
notificationsProvider ?? context?.read<NotificationsProvider>();
|
||||||
@ -587,8 +591,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
apps[id]!.app.preferredApkIndex = urlInd;
|
apps[id]!.app.preferredApkIndex = urlInd;
|
||||||
await saveApps([apps[id]!.app]);
|
await saveApps([apps[id]!.app]);
|
||||||
}
|
}
|
||||||
if (context != null ||
|
if (context != null || await canInstallSilently(apps[id]!.app)) {
|
||||||
await canInstallSilently(apps[id]!.app, settingsProvider)) {
|
|
||||||
appsToInstall.add(id);
|
appsToInstall.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -625,8 +628,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||||
}
|
}
|
||||||
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||||
bool willBeSilent =
|
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
|
||||||
await canInstallSilently(apps[appId]!.app, settingsProvider);
|
|
||||||
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
||||||
throw ObtainiumError(tr('cancelled'));
|
throw ObtainiumError(tr('cancelled'));
|
||||||
}
|
}
|
||||||
@ -675,8 +677,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
Directory appsDir = Directory(
|
Directory appsDir =
|
||||||
'${(await getExternalStorageDirectory())?.path as String}/app_data');
|
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
|
||||||
if (!appsDir.existsSync()) {
|
if (!appsDir.existsSync()) {
|
||||||
appsDir.createSync();
|
appsDir.createSync();
|
||||||
}
|
}
|
||||||
@ -876,8 +878,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.toList();
|
.toList();
|
||||||
// After reconciliation, delete externally uninstalled Apps if needed
|
// After reconciliation, delete externally uninstalled Apps if needed
|
||||||
if (removedAppIds.isNotEmpty) {
|
if (removedAppIds.isNotEmpty) {
|
||||||
var settingsProvider = SettingsProvider();
|
|
||||||
await settingsProvider.initializeSettings();
|
|
||||||
if (settingsProvider.removeOnExternalUninstall) {
|
if (settingsProvider.removeOnExternalUninstall) {
|
||||||
await removeApps(removedAppIds);
|
await removeApps(removedAppIds);
|
||||||
}
|
}
|
||||||
@ -916,6 +916,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
exportApps(isAuto: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeApps(List<String> appIds) async {
|
Future<void> removeApps(List<String> appIds) async {
|
||||||
@ -937,6 +938,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (appIds.isNotEmpty) {
|
if (appIds.isNotEmpty) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
exportApps(isAuto: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1093,32 +1095,51 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return updateAppIds;
|
return updateAppIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> exportApps() async {
|
Future<String?> exportApps(
|
||||||
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
|
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
|
||||||
if (await Permission.storage.isDenied) {
|
SettingsProvider settingsProvider = sp ?? this.settingsProvider;
|
||||||
await Permission.storage.request();
|
var exportDir = await settingsProvider.getExportDir();
|
||||||
|
if (isAuto) {
|
||||||
|
if (settingsProvider.autoExportOnChanges != true) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (await Permission.storage.isDenied) {
|
if (exportDir == null) {
|
||||||
throw ObtainiumError(tr('storagePermissionDenied'));
|
logs.add('Skipping auto-export as dir is not set.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logs.add('Started auto-export.');
|
||||||
|
var files = await saf
|
||||||
|
.listFiles(exportDir, columns: [saf.DocumentFileColumn.id])
|
||||||
|
.where((f) => f.uri.pathSegments.last.endsWith('-auto.json'))
|
||||||
|
.toList();
|
||||||
|
if (files.isNotEmpty) {
|
||||||
|
for (var f in files) {
|
||||||
|
saf.delete(f.uri);
|
||||||
|
}
|
||||||
|
logs.add('Previous auto-export deleted.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Directory? exportDir = Directory('/storage/emulated/0/Download');
|
if (exportDir == null || pickOnly) {
|
||||||
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
|
await settingsProvider.pickExportDir();
|
||||||
var downloadsAccessible = false;
|
exportDir = await settingsProvider.getExportDir();
|
||||||
try {
|
|
||||||
downloadsAccessible = exportDir.existsSync();
|
|
||||||
} catch (e) {
|
|
||||||
logs.add('Error accessing Downloads (will use fallback): $e');
|
|
||||||
}
|
}
|
||||||
if (!downloadsAccessible) {
|
if (exportDir == null) {
|
||||||
exportDir = await getExternalStorageDirectory();
|
return null;
|
||||||
path = exportDir!.path;
|
|
||||||
}
|
}
|
||||||
File export = File(
|
String? returnPath;
|
||||||
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
|
if (!pickOnly) {
|
||||||
export.writeAsStringSync(
|
var result = await saf.createFile(exportDir,
|
||||||
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
|
displayName:
|
||||||
return path;
|
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
content: jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
|
||||||
|
if (result == null) {
|
||||||
|
throw ObtainiumError(tr('unexpectedError'));
|
||||||
|
}
|
||||||
|
returnPath =
|
||||||
|
exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/');
|
||||||
|
}
|
||||||
|
return returnPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> importApps(String appsJSON) async {
|
Future<int> importApps(String appsJSON) async {
|
||||||
@ -1295,14 +1316,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
NotificationsProvider notificationsProvider = NotificationsProvider();
|
NotificationsProvider notificationsProvider = NotificationsProvider();
|
||||||
AppsProvider appsProvider = AppsProvider(isBg: true);
|
AppsProvider appsProvider = AppsProvider(isBg: true);
|
||||||
await appsProvider.loadApps();
|
await appsProvider.loadApps();
|
||||||
var settingsProvider = SettingsProvider();
|
|
||||||
await settingsProvider.initializeSettings();
|
|
||||||
|
|
||||||
int maxAttempts = 4;
|
int maxAttempts = 4;
|
||||||
|
|
||||||
params ??= {};
|
params ??= {};
|
||||||
if (params['toCheck'] == null) {
|
if (params['toCheck'] == null) {
|
||||||
settingsProvider.lastBGCheckTime = DateTime.now();
|
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
|
||||||
}
|
}
|
||||||
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
|
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
|
||||||
...(params['toCheck']
|
...(params['toCheck']
|
||||||
@ -1332,7 +1351,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
var didCompleteChecking = false;
|
var didCompleteChecking = false;
|
||||||
CheckingUpdatesNotification? notif;
|
CheckingUpdatesNotification? notif;
|
||||||
var networkRestricted = false;
|
var networkRestricted = false;
|
||||||
if (settingsProvider.bgUpdatesOnWiFiOnly) {
|
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
||||||
var netResult = await (Connectivity().checkConnectivity());
|
var netResult = await (Connectivity().checkConnectivity());
|
||||||
networkRestricted = (netResult != ConnectivityResult.wifi) &&
|
networkRestricted = (netResult != ConnectivityResult.wifi) &&
|
||||||
(netResult != ConnectivityResult.ethernet);
|
(netResult != ConnectivityResult.ethernet);
|
||||||
@ -1352,8 +1371,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
App? newApp = await appsProvider.checkUpdate(appId);
|
App? newApp = await appsProvider.checkUpdate(appId);
|
||||||
if (newApp != null) {
|
if (newApp != null) {
|
||||||
if (networkRestricted ||
|
if (networkRestricted ||
|
||||||
!(await appsProvider.canInstallSilently(
|
!(await appsProvider.canInstallSilently(app!.app))) {
|
||||||
app!.app, settingsProvider))) {
|
|
||||||
toNotify.add(newApp);
|
toNotify.add(newApp);
|
||||||
} else {
|
} else {
|
||||||
toInstall.add(MapEntry(appId, 0));
|
toInstall.add(MapEntry(appId, 0));
|
||||||
@ -1439,8 +1457,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
try {
|
try {
|
||||||
logs.add(
|
logs.add(
|
||||||
'BG install task $taskId: Attempting to update $appId in the background.');
|
'BG install task $taskId: Attempting to update $appId in the background.');
|
||||||
await appsProvider.downloadAndInstallLatestApps(
|
await appsProvider.downloadAndInstallLatestApps([appId], null,
|
||||||
[appId], null, settingsProvider,
|
|
||||||
notificationsProvider: notificationsProvider);
|
notificationsProvider: notificationsProvider);
|
||||||
await Future.delayed(const Duration(
|
await Future.delayed(const Duration(
|
||||||
seconds:
|
seconds:
|
||||||
|
@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart';
|
|||||||
import 'package:obtainium/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:shared_storage/shared_storage.dart' as saf;
|
||||||
|
|
||||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||||
String obtainiumId = 'dev.imranr.obtainium';
|
String obtainiumId = 'dev.imranr.obtainium';
|
||||||
@ -35,6 +37,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;
|
||||||
|
String? defaultAppDir;
|
||||||
bool justStarted = true;
|
bool justStarted = true;
|
||||||
|
|
||||||
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
String sourceUrl = 'https://github.com/ImranR98/Obtainium';
|
||||||
@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
// Not done in constructor as we want to be able to await it
|
// Not done in constructor as we want to be able to await it
|
||||||
Future<void> initializeSettings() async {
|
Future<void> initializeSettings() async {
|
||||||
prefs = await SharedPreferences.getInstance();
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
defaultAppDir = (await getExternalStorageDirectory())!.path;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,4 +352,58 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
prefs?.setBool('showDebugOpts', val);
|
prefs?.setBool('showDebugOpts', val);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get highlightTouchTargets {
|
||||||
|
return prefs?.getBool('highlightTouchTargets') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set highlightTouchTargets(bool val) {
|
||||||
|
prefs?.setBool('highlightTouchTargets', val);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri?> getExportDir() async {
|
||||||
|
var uriString = prefs?.getString('exportDir');
|
||||||
|
if (uriString != null) {
|
||||||
|
Uri? uri = Uri.parse(uriString);
|
||||||
|
if (!(await saf.canRead(uri) ?? false) ||
|
||||||
|
!(await saf.canWrite(uri) ?? false)) {
|
||||||
|
uri = null;
|
||||||
|
prefs?.remove('exportDir');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickExportDir({bool remove = false}) async {
|
||||||
|
var existingSAFPerms = (await saf.persistedUriPermissions()) ?? [];
|
||||||
|
var currentOneWayDataSyncDir = await getExportDir();
|
||||||
|
Uri? newOneWayDataSyncDir;
|
||||||
|
if (!remove) {
|
||||||
|
newOneWayDataSyncDir = (await saf.openDocumentTree());
|
||||||
|
}
|
||||||
|
if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) {
|
||||||
|
if (newOneWayDataSyncDir == null) {
|
||||||
|
prefs?.remove('exportDir');
|
||||||
|
} else {
|
||||||
|
prefs?.setString('exportDir', newOneWayDataSyncDir.toString());
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
for (var e in existingSAFPerms) {
|
||||||
|
await saf.releasePersistableUriPermission(e.uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get autoExportOnChanges {
|
||||||
|
return prefs?.getBool('autoExportOnChanges') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set autoExportOnChanges(bool val) {
|
||||||
|
prefs?.setBool('autoExportOnChanges', val);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart';
|
|||||||
import 'package:obtainium/app_sources/sourcehut.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/uptodown.dart';
|
||||||
import 'package:obtainium/app_sources/vlc.dart';
|
import 'package:obtainium/app_sources/vlc.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';
|
||||||
@ -363,15 +364,23 @@ abstract class AppSource {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String>? get requestHeaders => null;
|
Future<Map<String, String>?> getRequestHeaders(
|
||||||
|
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||||
|
bool forAPKDownload = false}) async {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> sourceRequest(String url,
|
Future<Response> sourceRequest(String url,
|
||||||
{bool followRedirects = true}) async {
|
{bool followRedirects = true,
|
||||||
|
Map<String, dynamic> additionalSettings =
|
||||||
|
const <String, dynamic>{}}) async {
|
||||||
|
var requestHeaders =
|
||||||
|
await getRequestHeaders(additionalSettings: additionalSettings);
|
||||||
if (requestHeaders != null || followRedirects == false) {
|
if (requestHeaders != null || followRedirects == false) {
|
||||||
var req = Request('GET', Uri.parse(url));
|
var req = Request('GET', Uri.parse(url));
|
||||||
req.followRedirects = followRedirects;
|
req.followRedirects = followRedirects;
|
||||||
if (requestHeaders != null) {
|
if (requestHeaders != null) {
|
||||||
req.headers.addAll(requestHeaders!);
|
req.headers.addAll(requestHeaders);
|
||||||
}
|
}
|
||||||
return Response.fromStream(await Client().send(req));
|
return Response.fromStream(await Client().send(req));
|
||||||
} else {
|
} else {
|
||||||
@ -512,6 +521,20 @@ regExValidator(String? value) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
intValidator(String? value, {bool positive = false}) {
|
||||||
|
if (value == null) {
|
||||||
|
return tr('invalidInput');
|
||||||
|
}
|
||||||
|
var num = int.tryParse(value);
|
||||||
|
if (num == null) {
|
||||||
|
return tr('invalidInput');
|
||||||
|
}
|
||||||
|
if (positive && num <= 0) {
|
||||||
|
return tr('invalidInput');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
// Add more source classes here so they are available via the service
|
// Add more source classes here so they are available via the service
|
||||||
List<AppSource> get sources => [
|
List<AppSource> get sources => [
|
||||||
@ -519,15 +542,16 @@ class SourceProvider {
|
|||||||
GitLab(),
|
GitLab(),
|
||||||
Codeberg(),
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
Jenkins(),
|
IzzyOnDroid(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
SourceHut(),
|
SourceHut(),
|
||||||
Aptoide(),
|
|
||||||
APKMirror(),
|
|
||||||
APKPure(),
|
APKPure(),
|
||||||
|
Aptoide(),
|
||||||
|
Uptodown(),
|
||||||
|
APKMirror(),
|
||||||
HuaweiAppGallery(),
|
HuaweiAppGallery(),
|
||||||
|
Jenkins(),
|
||||||
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
|
28
pubspec.lock
28
pubspec.lock
@ -538,18 +538,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: permission_handler
|
name: permission_handler
|
||||||
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
|
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.4.3"
|
version: "10.4.5"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3
|
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.3.4"
|
version: "10.3.6"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -562,10 +562,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_platform_interface
|
name: permission_handler_platform_interface
|
||||||
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
|
sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.11.3"
|
version: "3.11.5"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -686,6 +686,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.1"
|
||||||
|
shared_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_storage
|
||||||
|
sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -879,18 +887,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa"
|
sha256: "9427774649fd3c8b7ff53523051395d13aed2ca355822b822e6493d79f5fc05a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.5"
|
version: "3.10.0"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b"
|
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.1"
|
version: "2.6.0"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -17,10 +17,10 @@ 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.14.7+199 # When changing this, update the tag in main() accordingly
|
version: 0.14.13+205 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
@ -65,6 +65,7 @@ dependencies:
|
|||||||
flutter_archive: ^5.0.0
|
flutter_archive: ^5.0.0
|
||||||
hsluv: ^1.1.3
|
hsluv: ^1.1.3
|
||||||
connectivity_plus: ^4.0.2
|
connectivity_plus: ^4.0.2
|
||||||
|
shared_storage: ^0.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user