Compare commits

...

61 Commits

Author SHA1 Message Date
845eaab73e Merge pull request #909 from ImranR98/dev
Bugfix: HTML Source was broken (could not add apps)
2023-09-16 15:27:11 -04:00
298ff190ec Bugfix: HTML Source was broken (could not add apps) 2023-09-16 15:26:35 -04:00
734a548bc3 Merge pull request #906 from ImranR98/dev
Increment version
2023-09-16 09:28:57 -04:00
591973d97f Increment version 2023-09-16 09:28:45 -04:00
a2f57ecd66 Merge pull request #903 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-16 09:26:34 -04:00
13dcfb479d Merge pull request #902 from gidano/main
Update hu.json
2023-09-16 09:26:28 -04:00
3328d80130 Merge pull request #905 from saymanrifat/saymanrifat-721
Add back button to app detail page (#721)
2023-09-16 09:26:03 -04:00
f9df3ac0e2 Reduce top padding as we no longer need it 2023-09-16 09:25:26 -04:00
61d66e80d8 Add back button to app detail page (#721) 2023-09-16 18:31:57 +06:00
93e0360116 locale(pl): Update Polish translations 2023-09-16 12:45:50 +02:00
2cec5f7934 Update hu.json 2023-09-16 11:28:36 +02:00
8d4a09b007 Merge pull request #894 from markus-gitdev/main
Update de.json
2023-09-16 04:39:06 -04:00
6d8d2fa99d Merge branch 'main' into main 2023-09-16 04:38:48 -04:00
59ecd55530 Merge pull request #895 from DwainZwerg/patch-1
edit de.json
2023-09-16 04:38:05 -04:00
77fcc89e37 Merge branch 'main' into patch-1 2023-09-16 04:35:12 -04:00
7d63da4d9c Merge pull request #893 from bluefly000/japanese-translation
Update ja.json
2023-09-16 04:32:42 -04:00
8eea0bae99 Merge pull request #892 from iDazai/main
Update de.json
2023-09-16 04:32:35 -04:00
f5dcd8f776 Merge pull request #901 from ImranR98/dev
Fix background update issue (#896)
2023-09-16 04:31:29 -04:00
e1f2baeeda Fix background update issue (#896) 2023-09-16 04:31:03 -04:00
84d8ff8b0b Small improvement of the previous translation (especially "" instead of the English ''; my translations of the new features to be translated are mostly based on iDazai (https://github.com/ImranR98/Obtainium/pull/892/files) and markus-gitdev (https://github.com/ImranR98/Obtainium/pull/894/files)) 2023-09-14 08:46:51 +00:00
9862c22fdb Update de.json 2023-09-14 09:27:36 +02:00
8c82f53dd2 Update ja.json 2023-09-14 15:44:27 +09:00
5cf4c21ce3 Update de.json
Translated latest strings
2023-09-14 08:37:46 +02:00
4951f62d4a Merge pull request #890 from ImranR98/dev
- Fix export encoding (#887)
- Fix repeating background install for some apps (#886)
- More version filtering options for F-Droid (#885)
- Trim user URL input
2023-09-13 22:20:49 -04:00
4c0c4b7010 Trim user URL input 2023-09-13 22:19:09 -04:00
96051e614c Merge remote-tracking branch 'origin/main' into dev 2023-09-13 22:17:11 -04:00
a2e494b2ba More version filtering options for F-Droid (#885) 2023-09-13 22:15:10 -04:00
2d5a9bec84 Increment version, update packages 2023-09-13 21:27:18 -04:00
7d9571cfdd Fix repeating background install for some apps (#886) 2023-09-13 21:24:25 -04:00
acc6a780fa Fix export encoding (#887) 2023-09-13 21:01:39 -04:00
0e36d42a06 Merge pull request #863 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-09-13 19:22:57 -04:00
5cfddd807a locale(pl): Update Polish translations 2023-09-11 22:05:32 +02:00
17b5604f2a Merge pull request #882 from ImranR98/dev
Bugfix
2023-09-10 23:29:38 -04:00
fdb6eed6d0 Slight tweak 2023-09-10 23:28:37 -04:00
13de0437b8 Bugfix 2023-09-10 23:23:35 -04:00
a43c45f310 Merge pull request #881 from ImranR98/dev
Pick export dir + auto-export (#283, #600) + UI fix
2023-09-10 23:15:36 -04:00
9c56a4d1fc Bugs 2023-09-10 23:15:16 -04:00
2aea1d2631 Bugfixes 2023-09-10 23:11:06 -04:00
118e05a0fa Bugfix 2023-09-10 22:47:04 -04:00
05f497787e Build script tweak 2023-09-10 22:37:32 -04:00
53cf4d0234 Pick export dir + auto-export (#283, #600) 2023-09-10 22:35:28 -04:00
6e735b1763 Enable auto-export on update checks 2023-09-10 22:24:18 -04:00
873a1a0683 Allow for alternative app dirs (unfinished) 2023-09-10 15:55:34 -04:00
27b1149d1c Fix padding when touch targets not highlighted 2023-09-10 14:16:19 -04:00
c1e64f111e Merge pull request #878 from ImranR98/dev
Fix Codeberg not finding APKs (#877), Make touch highlight visible in dark theme (#860)
2023-09-10 14:03:48 -04:00
b2af8448fd Increment version 2023-09-10 14:02:26 -04:00
8f44338e76 Make touch highlight visible in dark theme (#860) 2023-09-10 13:56:39 -04:00
e4a55abcb3 Fix Codeberg not finding APKs (#877) 2023-09-10 13:49:04 -04:00
d7348b4973 Merge pull request #871 from ImranR98/dev
Make less obvious target highlighting optional
2023-09-09 07:30:31 -04:00
09421230f2 Make less obvious target highlighting optional 2023-09-09 07:29:35 -04:00
4596e32258 Merge pull request #870 from ImranR98/dev
- Add Support for Private GitHub Repos (#857)
- Add Uptodown Source (#853)
- Add User-Defined Version Extraction for HTML Source (#861)
- Make release notes tap target more obvious (#860)
- Fix Missing Download Notification on App Add (#858)
- Prevent Access Token from Showing in Logs (#714)
2023-09-09 06:54:12 -04:00
4dc007a4f6 Update packages, increment version 2023-09-09 06:50:13 -04:00
c53a156969 Slight UI tweak 2023-09-09 06:48:55 -04:00
94bd0774fb Make release notes tap target more obvious (#860) 2023-09-09 06:00:36 -04:00
b178b1d780 Bugfix 2023-09-09 05:46:56 -04:00
cbc840378c Add Uptodown (#853) 2023-09-09 05:31:04 -04:00
aa7989c16d Custom version extraction for HTML (#861) 2023-09-09 04:41:44 -04:00
85f9336804 Re-add dl notification when adding app (#858) 2023-09-09 03:58:54 -04:00
d66be3ecda Private GitHub repos now work (#857) 2023-09-09 03:55:52 -04:00
c08e05bd6c Trying to use header-based HTTP auth (not working) 2023-09-06 21:30:45 -04:00
e08ab89fd4 Update issue templates 2023-09-06 18:33:04 -04:00
39 changed files with 863 additions and 379 deletions

View File

@ -2,31 +2,31 @@
name: Bug report
about: Something isn't working right.
title: ''
labels: bug, To Check
labels: bug, to check
assignees: ''
---
**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**
A clear and concise description of what the bug is.
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
<!-- Steps to reproduce the behavior:
1. Go to '...'
2. Tap on '....'
3. Scroll down to '....'
4. See error
4. See error -->
**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:**
- Device: [e.g. Pixel 7]
- OS: [e.g. GrapheneOS]
- Obtainium Version [e.g. 0.14.6-beta]
- Device: <!-- [e.g. Pixel 7] -->
- OS: <!-- [e.g. GrapheneOS] -->
- Obtainium Version: <!-- [e.g. 0.14.6-beta] -->
**Additional context**
Add any other context about the problem here.
<!-- Add any other context about the problem here. -->

View File

@ -2,28 +2,28 @@
name: Feature request
about: Suggest a new Source, setting, or other feature.
title: ''
labels: enhancement, To Check
labels: enhancement, to check
assignees: ''
---
**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**
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):
- 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
- 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)**
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**
Add any other context or screenshots about the feature request here.
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -2,31 +2,36 @@
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)
Currently supported App sources:
- [GitHub](https://github.com/)
- [GitLab](https://gitlab.com/)
- [Codeberg](https://codeberg.org/)
- [F-Droid](https://f-droid.org/)
- [IzzyOnDroid](https://android.izzysoft.de/)
- [Mullvad](https://mullvad.net/en/)
- [Signal](https://signal.org/)
- [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/)
- [Aptoide](https://aptoide.com/)
- [APKMirror](https://apkmirror.com/) (Track-Only)
- [APKPure](https://apkpure.com/)
- [Huawei AppGallery](https://appgallery.huawei.com/)
- Third Party F-Droid Repos
- Jenkins Jobs
- [Steam](https://store.steampowered.com/mobile)
- [Telegram App](https://telegram.org)
- [Neutron Code](https://neutroncode.com)
- "HTML" (Fallback)
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
- Open Source - General:
- [GitHub](https://github.com/)
- [GitLab](https://gitlab.com/)
- [Codeberg](https://codeberg.org/)
- [F-Droid](https://f-droid.org/)
- Third Party F-Droid Repos
- [IzzyOnDroid](https://android.izzysoft.de/)
- [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/)
- Other - General:
- [APKPure](https://apkpure.com/)
- [Aptoide](https://aptoide.com/)
- [Uptodown](https://uptodown.com/)
- [APKMirror](https://apkmirror.com/) (Track-Only)
- [Huawei AppGallery](https://appgallery.huawei.com/)
- Jenkins Jobs
- Open Source - App-Specific:
- [Mullvad](https://mullvad.net/en/)
- [Signal](https://signal.org/)
- [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

View File

@ -12,8 +12,6 @@
"ok": "Ok",
"and": "e",
"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",
"fallbackToOlderReleases": "Retornar para versões anteriores",
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
"search": "Procurar",
"additionalOptsFor": "Opções Adicionais para {}",
"supportedSourcesBelow": "Fontes Compatíveis:",
"supportedSources": "Fontes Compatíveis",
"trackOnlyInBrackets": "(Apenas Seguir)",
"searchableInBrackets": "(Pesquisável)",
"appsString": "Apps",
@ -253,6 +251,13 @@
"exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remover App?",
"other": "Remover Apps?"

View File

@ -12,8 +12,6 @@
"ok": "Dobro",
"and": "i",
"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",
"fallbackToOlderReleases": "Povratak na starija izdanja",
"filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
"search": "Pretraživanje",
"additionalOptsFor": "Dodatne opcije za {}",
"supportedSourcesBelow": "Podržani izvori:",
"supportedSources": "Podržani izvori",
"trackOnlyInBrackets": "(Samo za praćenje)",
"searchableInBrackets": "(Može se pretraživati)",
"appsString": "Aplikacije",
@ -113,7 +111,7 @@
"dark": "Tamna",
"light": "Svijetla",
"followSystem": "Pratite sistem",
"obtainium": "Obtainium",
"obtainium": "Obtainium",
"materialYou": "Material You",
"useBlackTheme": "Koristite čisto crnu tamnu temu",
"appSortBy": "Aplikacije sortirane po",
@ -250,7 +248,14 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"removeAppQuestion": {
"versionExtractionRegEx": "Version Extraction RegEx",
"matchGroupToUse": "Match Group to Use",
"highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"
},

View File

@ -12,8 +12,6 @@
"ok": "Okay",
"and": "und",
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
"githubPATFormat": "Benutzername:Token",
"includePrereleases": "Vorabversionen einbeziehen",
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
@ -30,8 +28,8 @@
"xIsTrackOnly": "{} ist nur zur Nachverfolgung",
"source": "Quelle",
"app": "App",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.",
"youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.",
"appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.",
"youPickedTrackOnly": "Sie haben die Option Nur Nachverfolgen gewählt.",
"trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.",
"cancelled": "Abgebrochen",
"appAlreadyAdded": "App bereits hinzugefügt",
@ -40,10 +38,10 @@
"appSourceURL": "Quell-URL der App",
"error": "Fehler",
"add": "Hinzufügen",
"searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
"searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)",
"search": "Suchen",
"additionalOptsFor": "Zusatzoptionen für {}",
"supportedSourcesBelow": "Unterstützte Quellen:",
"supportedSources": "Unterstützte Quellen",
"trackOnlyInBrackets": "(Nur Nachverfolgen)",
"searchableInBrackets": "(Durchsuchbar)",
"appsString": "Apps",
@ -72,7 +70,7 @@
"yes": "Ja",
"markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell",
"pinToTop": "Oben anheften",
"unpinFromTop": "'Oben anheften' aufheben",
"unpinFromTop": "Oben anheften aufheben",
"resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?",
"installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.",
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
@ -177,7 +175,7 @@
"appId": "App ID",
"appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden",
"reposHaveMultipleApps": "Repos können mehrere Apps enthalten",
"fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
"fdroidThirdPartyRepo": "F-Droid Drittparteienrepo",
"steam": "Steam",
"steamMobile": "Steam Mobile",
"steamChat": "Steam Chat",
@ -246,10 +244,17 @@
"xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.",
"backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.",
"backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
"verifyLatestTag": "Überprüfe das 'latest' Tag",
"verifyLatestTag": "Überprüfe das latest Tag",
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen",
"versionExtractionRegEx": "Versions-Extraktion RegEx",
"matchGroupToUse": "Zu verwendende Gruppe abgleichen",
"highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung",
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
"trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"

View File

@ -12,8 +12,6 @@
"ok": "Okay",
"and": "and",
"githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
"githubPATHint": "PAT must be in this format: username:token",
"githubPATFormat": "username:token",
"includePrereleases": "Include prereleases",
"fallbackToOlderReleases": "Fallback to older releases",
"filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Search (Some Sources Only)",
"search": "Search",
"additionalOptsFor": "Additional Options for {}",
"supportedSourcesBelow": "Supported Sources:",
"supportedSources": "Supported Sources",
"trackOnlyInBrackets": "(Track-Only)",
"searchableInBrackets": "(Searchable)",
"appsString": "Apps",
@ -253,6 +251,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"

View File

@ -12,8 +12,6 @@
"ok": "Correcto",
"and": "y",
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
"githubPATFormat": "nombre_de_usuario:token",
"includePrereleases": "Incluir versiones preliminares",
"fallbackToOlderReleases": "Retorceder a versiones previas",
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
"search": "Buscar",
"additionalOptsFor": "Opciones Adicionales para {}",
"supportedSourcesBelow": "Fuentes Soportadas:",
"supportedSources": "Fuentes Soportadas",
"trackOnlyInBrackets": "(Solo Seguimiento)",
"searchableInBrackets": "(Soporta Búsquedas)",
"appsString": "Aplicaciones",
@ -250,6 +248,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"

View File

@ -12,8 +12,6 @@
"ok": "باشه",
"and": "و",
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
"githubPATHint": "PAT باید در این قالب باشد: username:token",
"githubPATFormat": "username:token",
"includePrereleases": "شامل نسخه های اولیه",
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
"search": "جستجو کردن",
"additionalOptsFor": "گزینه های اضافی برای {}",
"supportedSourcesBelow": "منابع پشتیبانی شده:",
"supportedSources": "منابع پشتیبانی شده",
"trackOnlyInBrackets": "«فقط ردیابی»",
"searchableInBrackets": "(قابل جستجو)",
"appsString": "برنامه ها",
@ -250,6 +248,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"

View File

@ -12,8 +12,6 @@
"ok": "Okay",
"and": "et",
"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",
"fallbackToOlderReleases": "Retour aux anciennes versions",
"filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
"search": "Rechercher",
"additionalOptsFor": "Options supplémentaires pour {}",
"supportedSourcesBelow": "Sources prises en charge :",
"supportedSources": "Sources prises en charge ",
"trackOnlyInBrackets": "(Suivi uniquement)",
"searchableInBrackets": "(Recherchable)",
"appsString": "Applications",
@ -250,6 +248,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"

View File

@ -12,8 +12,6 @@
"ok": "Oké",
"and": "és",
"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",
"fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
"filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Keresés (csak egyes források)",
"search": "Keresés",
"additionalOptsFor": "További lehetőségek a következőhöz: {}",
"supportedSourcesBelow": "Támogatott források:",
"supportedSources": "Támogatott források",
"trackOnlyInBrackets": "(Csak nyomonkövetés)",
"searchableInBrackets": "(Kereshető)",
"appsString": "Appok",
@ -248,7 +246,14 @@
"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)",
"bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
"versionExtractionRegEx": "Verzió kibontása reguláris kifejezéssel",
"matchGroupToUse": "Párosítsa a csoportot a használathoz",
"highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
"pickExportDir": "Válassza az Exportálási könyvtárat",
"autoExportOnChanges": "Auto-exportálás a változások után",
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"

View File

@ -12,8 +12,6 @@
"ok": "Va bene",
"and": "e",
"githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
"githubPATHint": "PAT deve seguire questo formato: nomeutente:token",
"githubPATFormat": "nomeutente:token",
"includePrereleases": "Includi prerelease",
"fallbackToOlderReleases": "Ripiega su release precedenti",
"filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
"search": "Cerca",
"additionalOptsFor": "Opzioni aggiuntive per {}",
"supportedSourcesBelow": "Fonti supportate:",
"supportedSources": "Fonti supportate",
"trackOnlyInBrackets": "(Solo-Monitoraggio)",
"searchableInBrackets": "(ricercabile)",
"appsString": "App",
@ -250,6 +248,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"

View File

@ -12,8 +12,6 @@
"ok": "OK",
"and": "と",
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
"githubPATFormat": "ユーザー名:トークン",
"includePrereleases": "プレリリースを含む",
"fallbackToOlderReleases": "旧リリースへのフォールバック",
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
"search": "検索",
"additionalOptsFor": "{}の追加オプション",
"supportedSourcesBelow": "対応するソース:",
"supportedSources": "対応するソース",
"trackOnlyInBrackets": "(追跡のみ)",
"searchableInBrackets": "(検索可能)",
"appsString": "アプリ",
@ -146,7 +144,7 @@
"updatesAvailable": "アップデートが利用可能",
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
"noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{} のアップデートが利用可能です",
"xHasAnUpdate": "{} のアップデートが利用可能です",
"appsUpdated": "アプリをアップデートしました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{} が {} にアップデートされました",
@ -243,14 +241,23 @@
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
"appsPossiblyUpdated": "アプリのアップデートを試行",
"appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する",
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります",
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります",
"enableBackgroundUpdates": "バックグラウンドアップデートを有効化する",
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
"verifyLatestTag": "'latest'タグを確認する",
"intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
"intermediateLinkNotFound": "中間リンクが見つかりませんでした",
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合バックグラウンドアップデートを無効にする",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
"versionExtractionRegEx": "バージョン抽出の正規表現",
"matchGroupToUse": "使用するマッチしたグループ",
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
"pickExportDir": "エクスポートディレクトリを選択",
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"
@ -292,15 +299,15 @@
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
},
"xAndNMoreUpdatesInstalled": {
"one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました"
"one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります"
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります"
}
}

View File

@ -22,8 +22,6 @@
"ok": "Okej",
"and": "i",
"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",
"fallbackToOlderReleases": "Powracaj do starszych wersji",
"filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
@ -52,7 +50,7 @@
"searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
"search": "Szukaj",
"additionalOptsFor": "Dodatkowe opcje dla {}",
"supportedSourcesBelow": "Obsługiwane źródła:",
"supportedSources": "Obsługiwane źródła",
"trackOnlyInBrackets": "(tylko obserwowane)",
"searchableInBrackets": "(Wyszukiwalne)",
"appsString": "Aplikacje",
@ -255,7 +253,14 @@
"verifyLatestTag": "Zweryfikuj najnowszy tag",
"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",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
"autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
"versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję",
"matchGroupToUse": "Dopasuj grupę do użycia",
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
"removeAppQuestion": {
"one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?",

View File

@ -12,8 +12,6 @@
"ok": "Окей",
"and": "и",
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
"githubPATFormat": "имя_пользователя:токен",
"includePrereleases": "Включить предварительные релизы",
"fallbackToOlderReleases": "Откатиться к более старым версиям",
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
"search": "Поиск",
"additionalOptsFor": "Дополнительные опции для {}",
"supportedSourcesBelow": "Поддерживаемые источники:",
"supportedSources": "Поддерживаемые источники",
"trackOnlyInBrackets": "(Только для отслеживания)",
"searchableInBrackets": "(Поиск)",
"appsString": "Приложения",
@ -250,6 +248,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"

View File

@ -12,8 +12,6 @@
"ok": "好的",
"and": "和",
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
"githubPATFormat": "username:token",
"includePrereleases": "包含预发行版",
"fallbackToOlderReleases": "将旧发行版作为备选",
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
@ -43,7 +41,7 @@
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
"search": "搜索",
"additionalOptsFor": "{} 的更多选项",
"supportedSourcesBelow": "支持的来源",
"supportedSources": "支持的来源",
"trackOnlyInBrackets": "(仅追踪)",
"searchableInBrackets": "(可搜索)",
"appsString": "应用列表",
@ -251,6 +249,13 @@
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"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",
"filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"

View File

@ -4,7 +4,9 @@
CURR_DIR="$(pwd)"
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
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

View File

@ -25,12 +25,16 @@ class APKCombo extends AppSource {
}
@override
Map<String, String> get requestHeaders => {
"User-Agent": "curl/8.0.1",
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
};
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
return {
"User-Agent": "curl/8.0.1",
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
};
}
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
var res = await sourceRequest('$standardUrl/download/apk');

View File

@ -3,6 +3,21 @@ import 'package:html/parser.dart';
import 'package:obtainium/custom_errors.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 {
APKPure() {
host = 'apkpure.com';
@ -47,17 +62,7 @@ class APKPure extends AppSource {
}
String? dateString =
html.querySelector('span.info-other span.date')?.text.trim();
DateTime? releaseDate;
try {
releaseDate = dateString != null
? DateFormat('MMM dd, yyyy').parse(dateString)
: null;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: null;
} catch (err) {
// ignore
}
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
List<MapEntry<String, String>> apkUrls = [
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
@ -70,11 +75,13 @@ class APKPure extends AppSource {
Uri.parse(standardUrl).pathSegments.reversed.last;
String appName =
html.querySelector('h1.info-title')?.text.trim() ?? appId;
String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml
.trim().replaceAll("<br>", " \n");
String? changeLog = htmlChangelog
.querySelector("div.whats-new-info p:not(.date)")
?.innerHtml
.trim()
.replaceAll("<br>", " \n");
return APKDetails(version, apkUrls, AppNames(author, appName),
releaseDate: releaseDate,
changeLog: changeLog);
releaseDate: releaseDate, changeLog: changeLog);
} else {
throw getObtainiumHttpError(res);
}

View File

@ -1,8 +1,6 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
@ -75,34 +73,4 @@ class Aptoide extends AppSource {
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
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);
}
}
}

View File

@ -13,10 +13,24 @@ class FDroid extends AppSource {
name = tr('fdroid');
canSearch = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormTextField('filterVersionsByRegEx',
label: tr('filterVersionsByRegEx'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
],
[
GeneratedFormSwitch('trySelectingSuggestedVersionCode',
label: tr('trySelectingSuggestedVersionCode'), defaultValue: true)
],
[
GeneratedFormSwitch('autoSelectHighestVersionCode',
label: tr('autoSelectHighestVersionCode'))
]
],
];
}
@ -45,25 +59,73 @@ class FDroid extends AppSource {
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl,
{bool autoSelectHighestVersionCode = false}) {
{bool autoSelectHighestVersionCode = false,
bool trySelectingSuggestedVersionCode = false,
String? filterVersionsByRegEx}) {
if (res.statusCode == 200) {
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
var response = jsonDecode(res.body);
List<dynamic> releases = response['packages'] ?? [];
if (releases.isEmpty) {
throw NoReleasesError();
}
String? latestVersion = releases[0]['versionName'];
if (latestVersion == null) {
String? version;
Iterable<dynamic> releaseChoices = [];
// Grab the versionCode suggested if the user chose to do that
// Only do so at this stage if the user has no release filter
if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null &&
filterVersionsByRegEx == null) {
var suggestedReleases = releases.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
}
}
// Apply the release filter if any
if (filterVersionsByRegEx != null) {
version = null;
releaseChoices = [];
for (var i = 0; i < releases.length; i++) {
if (RegExp(filterVersionsByRegEx)
.hasMatch(releases[i]['versionName'])) {
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
Iterable<dynamic> latestReleases =
releases.where((element) => element['versionName'] == latestVersion);
if (latestReleases.length > 1 && autoSelectHighestVersionCode) {
latestReleases = [latestReleases.first];
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
List<String> apkUrls = latestReleases
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
}
}
}
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
return APKDetails(version, getApkUrlsFromUrls(apkUrls),
AppNames(name, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
@ -82,7 +144,15 @@ class FDroid extends AppSource {
'https://$host/repo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx:
(additionalSettings['filterVersionsByRegEx'] as String?)
?.isNotEmpty ==
true
? additionalSettings['filterVersionsByRegEx']
: null);
}
@override

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
@ -21,20 +22,6 @@ class GitHub extends AppSource {
label: tr('githubPATLabel'),
password: true,
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'),
belowWidgets: [
const SizedBox(
@ -169,26 +156,53 @@ class GitHub extends AppSource {
return url.substring(0, match.end);
}
Future<String> getCredentialPrefixIfAny(
Map<String, dynamic> additionalSettings) async {
@override
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();
await settingsProvider.initializeSettings();
var sourceConfig =
await getSourceConfigValues(additionalSettings, settingsProvider);
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
Future<String?> getSourceNote() async {
if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) {
if (!hostChanged && (await getTokenIfAny({})) == null) {
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
}
return null;
}
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host';
'https://api.$host';
Future<String> convertStandardUrlToAPIUrl(
String standardUrl, Map<String, dynamic> additionalSettings) async =>
@ -238,9 +252,10 @@ class GitHub extends AppSource {
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?)
?.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,
e['browser_download_url'] as String)
(e['url'] ?? e['browser_download_url']) as String)
: const MapEntry('', '');
})
.where((element) => element.key.toLowerCase().endsWith('.apk'))

View File

@ -1,4 +1,5 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart';
@ -109,6 +110,26 @@ class HTML extends AppSource {
hint: '([0-9]+\.)*[0-9]+/\$',
required: false,
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) {
if (value?.isEmpty == true) {
value = null;
}
value ??= '1';
return intValidator(value);
}
])
]
];
overrideVersionDetectionFormDefault('noVersionDetection',
@ -116,11 +137,14 @@ class HTML extends AppSource {
}
@override
// TODO: implement requestHeaders choice, hardcoded for now
Map<String, String>? get requestHeaders => {
"User-Agent":
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
};
Future<Map<String, String>?> getRequestHeaders(
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
bool forAPKDownload = false}) async {
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
String sourceSpecificStandardizeURL(String url) {
@ -180,10 +204,23 @@ class HTML extends AppSource {
throw NoReleasesError();
}
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 =
[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')));
} else {
throw getObtainiumHttpError(res);

View File

@ -40,6 +40,9 @@ class IzzyOnDroid extends AppSource {
'https://android.izzysoft.de/frepo/$appId',
standardUrl,
autoSelectHighestVersionCode:
additionalSettings['autoSelectHighestVersionCode'] == true);
additionalSettings['autoSelectHighestVersionCode'] == true,
trySelectingSuggestedVersionCode:
additionalSettings['trySelectingSuggestedVersionCode'] == true,
filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']);
}
}

View 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);
}
}

View File

@ -12,7 +12,12 @@ class VLC extends AppSource {
get dwUrlBase => 'https://get.$host/vlc-android/';
@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
String sourceSpecificStandardizeURL(String url) {

View File

@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
late int max;
late String? hint;
late bool password;
late TextInputType? textInputType;
GeneratedFormTextField(String key,
{String label = 'Input',
@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
this.required = true,
this.max = 1,
this.hint,
this.password = false})
this.password = false,
this.textInputType})
: super(key,
label: label,
belowWidgets: belowWidgets,
@ -144,7 +146,8 @@ Color generateRandomLightColor() {
// Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
// 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]);
}
@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField(
keyboardType: formItem.textInputType,
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> {
}) ??
[const SizedBox.shrink()],
(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)
?.values
.where((e) => e.value)
.length == 1
as Map<String, MapEntry<int, bool>>?)
?.values
.where((e) => e.value)
.length ==
1
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
onPressed: () {
setState(() {
var temp = values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>;
// get selected category str where bool is true
final oldEntry = temp.entries.firstWhere((entry) => entry.value.value);
// generate new color, ensure it is not the same
int newColor = oldEntry.value.key;
while(oldEntry.value.key == newColor) {
newColor = generateRandomLightColor().value;
}
// Update entry with new color, remain selected
temp.update(oldEntry.key, (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'),
))
padding: const EdgeInsets.symmetric(horizontal: 4),
child: IconButton(
onPressed: () {
setState(() {
var temp = values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>;
// get selected category str where bool is true
final oldEntry = temp.entries
.firstWhere((entry) => entry.value.value);
// generate new color, ensure it is not the same
int newColor = oldEntry.value.key;
while (oldEntry.value.key == newColor) {
newColor = generateRandomLightColor().value;
}
// Update entry with new color, remain selected
temp.update(oldEntry.key,
(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'),
))
: const SizedBox.shrink(),
(values[widget.items[r][e].key]
as Map<String, MapEntry<int, bool>>?)

View File

@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports
import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.14.10';
const String currentVersion = '0.14.17';
const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource {
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
String username, int page) async {
Response res = await get(Uri.parse(
'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page'));
Response res = await get(
Uri.parse(
'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
headers: await GitHub().getRequestHeaders());
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) {

View File

@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart';
import 'package:obtainium/pages/import_export.dart';
import 'package:obtainium/pages/settings.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/source_provider.dart';
import 'package:provider/provider.dart';
@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> {
Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
NotificationsProvider notificationsProvider =
context.read<NotificationsProvider>();
bool doingSomething = gettingAppInfo || searching;
@ -145,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> {
userPickedTrackOnly))) {
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
app = await sourceProvider.getApp(
pickedSource!, userInput, additionalSettings,
pickedSource!, userInput.trim(), additionalSettings,
trackOnlyOverride: trackOnly,
overrideSource: pickedSourceOverride,
inferAppIdIfOptional: inferAppIdIfOptional);
@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> {
app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
// ignore: use_build_context_synchronously
var downloadedArtifact = await appsProvider.downloadApp(
app, globalNavigatorKey.currentContext);
app, globalNavigatorKey.currentContext,
notificationsProvider: notificationsProvider);
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) {
@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 48,
),
Text(
tr('supportedSourcesBelow'),
tr('supportedSources'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(
height: 8,
height: 16,
),
...sourceProvider.sources
.map((e) => GestureDetector(
@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> {
: const SizedBox();
},
future: pickedSource?.getSourceNote()),
const SizedBox(
height: 16,
SizedBox(
height: pickedSource != null ? 16 : 96,
),
if (pickedSource != null)
getAdditionalOptsCol()
else
getSourcesListWidget(),
const SizedBox(
height: 8,
if (pickedSource != null) getAdditionalOptsCol(),
if (pickedSource == null)
const Divider(
height: 48,
),
if (pickedSource == null) getSourcesListWidget(),
SizedBox(
height: pickedSource != null ? 8 : 2,
),
])),
)

View File

@ -152,7 +152,7 @@ class _AppPageState extends State<AppPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 125),
const SizedBox(height: 20),
app?.icon != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory(
@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> {
try {
HapticFeedback.heavyImpact();
var res = await appsProvider.downloadAndInstallLatestApps(
app?.app.id != null ? [app!.app.id] : [],
globalNavigatorKey.currentContext,
settingsProvider);
app?.app.id != null ? [app!.app.id] : [],
globalNavigatorKey.currentContext,
);
if (app?.app.installedVersion != null && !trackOnly) {
// ignore: use_build_context_synchronously
showError(tr('appsUpdated'), context);
@ -463,9 +463,18 @@ class _AppPageState extends State<AppPage> {
: null))
],
));
appScreenAppBar() => AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
);
return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(),
backgroundColor: Theme.of(context).colorScheme.surface,
body: RefreshIndicator(
child: settingsProvider.showAppWebpage

View File

@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> {
: () {
appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id],
globalNavigatorKey.currentContext,
settingsProvider).catchError((e) {
globalNavigatorKey.currentContext).catchError((e) {
showError(e, context);
return <String>[];
});
@ -449,33 +448,48 @@ class AppsPageState extends State<AppsPage> {
: const SizedBox.shrink(),
GestureDetector(
onTap: showChangesFn,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width / 4),
child: Text(getVersionText(index),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end)),
]),
Row(
mainAxisSize: MainAxisSize.min,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: settingsProvider.highlightTouchTargets &&
showChangesFn != null
? (Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).primaryColorLight)
.withAlpha(20)
: null),
padding: settingsProvider.highlightTouchTargets
? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
: const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
getChangesButtonString(index, showChangesFn != null),
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: showChangesFn != null
? TextDecoration.underline
: TextDecoration.none),
)
Row(mainAxisSize: MainAxisSize.min, children: [
Container(
constraints: BoxConstraints(
maxWidth:
MediaQuery.of(context).size.width / 4),
child: Text(getVersionText(index),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end)),
]),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
getChangesButtonString(
index, showChangesFn != null),
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: showChangesFn != null
? TextDecoration.underline
: TextDecoration.none),
)
],
),
],
),
],
))
)))
],
);
@ -684,8 +698,8 @@ class AppsPageState extends State<AppsPage> {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(toInstall,
globalNavigatorKey.currentContext, settingsProvider)
.downloadAndInstallLatestApps(
toInstall, globalNavigatorKey.currentContext)
.catchError((e) {
showError(e, context);
return <String>[];

View File

@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
@override
Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>();
var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all(
@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> {
});
}
runObtainiumExport() {
runObtainiumExport() async {
HapticFeedback.selectionClick();
appsProvider.exportApps().then((String path) {
showError(tr('exportedTo', args: [path]), context);
appsProvider
.exportApps(
pickOnly: (await settingsProvider.getExportDir()) == null,
sp: settingsProvider)
.then((String? result) {
if (result != null) {
showError(tr('exportedTo', args: [result]), context);
}
}).catchError((e) {
showError(e, context);
});
@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr('obtainiumExport')))),
const SizedBox(
width: 16,
),
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: importInProgress
? null
: runObtainiumImport,
child: Text(tr('obtainiumImport'))))
],
FutureBuilder(
future: settingsProvider.getExportDir(),
builder: (context, snapshot) {
return Column(
children: [
Row(
children: [
Expanded(
child: TextButton(
style: outlineButtonStyle,
onPressed: appsProvider.apps.isEmpty ||
importInProgress
? null
: runObtainiumExport,
child: Text(tr(snapshot.data != null
? 'obtainiumExport'
: 'pickExportDir')),
)),
const SizedBox(
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)
const Column(
@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
fontStyle: FontStyle.italic, fontSize: 12)),
const SizedBox(
height: 8,
)
),
],
)))
]));

View File

@ -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,
Text(
tr('categories'),

View File

@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.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:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
final pm = AndroidPackageManager();
@ -149,6 +151,7 @@ class AppsProvider with ChangeNotifier {
late Stream<FGBGType>? foregroundStream;
late StreamSubscription<FGBGType>? foregroundSubscription;
late Directory APKDir;
late SettingsProvider settingsProvider = SettingsProvider();
Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy());
@ -160,6 +163,7 @@ class AppsProvider with ChangeNotifier {
if (isForeground) await loadApps();
});
() async {
await settingsProvider.initializeSettings();
var cacheDirs = await getExternalCacheDirectories();
if (cacheDirs?.isNotEmpty ?? false) {
APKDir = cacheDirs!.first;
@ -215,7 +219,7 @@ class AppsProvider with ChangeNotifier {
if (headers != null) {
req.headers.addAll(headers);
}
var client = Client();
var client = http.Client();
StreamedResponse response = await client.send(req);
String ext =
response.headers['content-disposition']?.split('.').last ?? 'apk';
@ -298,9 +302,11 @@ class AppsProvider with ChangeNotifier {
notificationsProvider?.cancel(notif.id);
int? prevProg;
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
var headers = await source.getRequestHeaders(
additionalSettings: app.additionalSettings, forAPKDownload: true);
var downloadedFile = await downloadFileWithRetry(
downloadUrl, fileNameNoExt, headers: source.requestHeaders,
(double? progress) {
downloadUrl, fileNameNoExt,
headers: headers, (double? progress) {
int? prog = progress?.ceil();
if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress;
@ -366,8 +372,7 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null)
.isNotEmpty;
Future<bool> canInstallSilently(
App app, SettingsProvider settingsProvider) async {
Future<bool> canInstallSilently(App app) async {
if (app.id == obtainiumId) {
return false;
}
@ -425,7 +430,8 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<void> installXApkDir(DownloadedXApkDir dir) async {
Future<void> installXApkDir(DownloadedXApkDir dir,
{bool needsBGWorkaround = false}) async {
// We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered
@ -438,7 +444,8 @@ class AppsProvider with ChangeNotifier {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file));
await installApk(DownloadedApk(dir.appId, file),
needsBGWorkaround: needsBGWorkaround);
} catch (e) {
logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
@ -458,7 +465,8 @@ class AppsProvider with ChangeNotifier {
}
}
Future<bool> installApk(DownloadedApk file) async {
Future<bool> installApk(DownloadedApk file,
{bool needsBGWorkaround = false}) async {
var newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: file.file.path);
PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id);
@ -467,6 +475,17 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) {
throw DowngradeError();
}
if (needsBGWorkaround) {
// The below 'await' will never return if we are in a background process
// To work around this, we should assume the install will be successful
// So we update the app's installed version first as we will never get to the later code
// We can't conditionally get rid of the 'await' as this causes install fails (BG process times out) - see #896
// TODO: When fixed, update this function and the calls to it accordingly
apps[file.appId]!.app.installedVersion =
apps[file.appId]!.app.latestVersion;
await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false);
}
int? code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
bool installed = false;
@ -536,7 +555,6 @@ class AppsProvider with ChangeNotifier {
getHost(apkUrl.value) != getHost(app.url) &&
context != null) {
// ignore: use_build_context_synchronously
var settingsProvider = context.read<SettingsProvider>();
if (!(settingsProvider.hideAPKOriginWarning) &&
// ignore: use_build_context_synchronously
await showDialog(
@ -557,8 +575,8 @@ class AppsProvider with ChangeNotifier {
// 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
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(List<String> appIds,
BuildContext? context, SettingsProvider settingsProvider,
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{NotificationsProvider? notificationsProvider}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
@ -587,8 +605,7 @@ class AppsProvider with ChangeNotifier {
apps[id]!.app.preferredApkIndex = urlInd;
await saveApps([apps[id]!.app]);
}
if (context != null ||
await canInstallSilently(apps[id]!.app, settingsProvider)) {
if (context != null || await canInstallSilently(apps[id]!.app)) {
appsToInstall.add(id);
}
}
@ -625,8 +642,7 @@ class AppsProvider with ChangeNotifier {
downloadedDir = downloadedArtifact as DownloadedXApkDir;
}
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent =
await canInstallSilently(apps[appId]!.app, settingsProvider);
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
@ -639,15 +655,13 @@ class AppsProvider with ChangeNotifier {
try {
if (downloadedFile != null) {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installApk(downloadedFile);
installApk(downloadedFile, needsBGWorkaround: true);
} else {
await installApk(downloadedFile);
}
} else {
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installXApkDir(downloadedDir!);
installXApkDir(downloadedDir!, needsBGWorkaround: true);
} else {
await installXApkDir(downloadedDir!);
}
@ -675,8 +689,8 @@ class AppsProvider with ChangeNotifier {
}
Future<Directory> getAppsDir() async {
Directory appsDir = Directory(
'${(await getExternalStorageDirectory())?.path as String}/app_data');
Directory appsDir =
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
if (!appsDir.existsSync()) {
appsDir.createSync();
}
@ -876,8 +890,6 @@ class AppsProvider with ChangeNotifier {
.toList();
// After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds);
}
@ -916,6 +928,7 @@ class AppsProvider with ChangeNotifier {
}
}
notifyListeners();
exportApps(isAuto: true);
}
Future<void> removeApps(List<String> appIds) async {
@ -937,6 +950,7 @@ class AppsProvider with ChangeNotifier {
}
if (appIds.isNotEmpty) {
notifyListeners();
exportApps(isAuto: true);
}
}
@ -1093,32 +1107,52 @@ class AppsProvider with ChangeNotifier {
return updateAppIds;
}
Future<String> exportApps() async {
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) {
if (await Permission.storage.isDenied) {
await Permission.storage.request();
Future<String?> exportApps(
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
SettingsProvider settingsProvider = sp ?? this.settingsProvider;
var exportDir = await settingsProvider.getExportDir();
if (isAuto) {
if (settingsProvider.autoExportOnChanges != true) {
return null;
}
if (await Permission.storage.isDenied) {
throw ObtainiumError(tr('storagePermissionDenied'));
if (exportDir == null) {
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');
String path = 'Downloads'; // TODO: See if hardcoding this can be avoided
var downloadsAccessible = false;
try {
downloadsAccessible = exportDir.existsSync();
} catch (e) {
logs.add('Error accessing Downloads (will use fallback): $e');
if (exportDir == null || pickOnly) {
await settingsProvider.pickExportDir();
exportDir = await settingsProvider.getExportDir();
}
if (!downloadsAccessible) {
exportDir = await getExternalStorageDirectory();
path = exportDir!.path;
if (exportDir == null) {
return null;
}
File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync(
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()));
return path;
String? returnPath;
if (!pickOnly) {
var result = await saf.createFile(exportDir,
displayName:
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json',
bytes: Uint8List.fromList(utf8.encode(
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 {
@ -1295,14 +1329,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
NotificationsProvider notificationsProvider = NotificationsProvider();
AppsProvider appsProvider = AppsProvider(isBg: true);
await appsProvider.loadApps();
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
int maxAttempts = 4;
params ??= {};
if (params['toCheck'] == null) {
settingsProvider.lastBGCheckTime = DateTime.now();
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
}
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
...(params['toCheck']
@ -1332,7 +1364,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
var didCompleteChecking = false;
CheckingUpdatesNotification? notif;
var networkRestricted = false;
if (settingsProvider.bgUpdatesOnWiFiOnly) {
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
var netResult = await (Connectivity().checkConnectivity());
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
@ -1352,8 +1384,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
App? newApp = await appsProvider.checkUpdate(appId);
if (newApp != null) {
if (networkRestricted ||
!(await appsProvider.canInstallSilently(
app!.app, settingsProvider))) {
!(await appsProvider.canInstallSilently(app!.app))) {
toNotify.add(newApp);
} else {
toInstall.add(MapEntry(appId, 0));
@ -1439,8 +1470,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
try {
logs.add(
'BG install task $taskId: Attempting to update $appId in the background.');
await appsProvider.downloadAndInstallLatestApps(
[appId], null, settingsProvider,
await appsProvider.downloadAndInstallLatestApps([appId], null,
notificationsProvider: notificationsProvider);
await Future.delayed(const Duration(
seconds:

View File

@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/providers/apps_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:shared_preferences/shared_preferences.dart';
import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
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 {
SharedPreferences? prefs;
String? defaultAppDir;
bool justStarted = true;
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
Future<void> initializeSettings() async {
prefs = await SharedPreferences.getInstance();
defaultAppDir = (await getExternalStorageDirectory())!.path;
notifyListeners();
}
@ -348,4 +352,58 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('showDebugOpts', val);
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();
}
}

View File

@ -26,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/sourcehut.dart';
import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/app_sources/uptodown.dart';
import 'package:obtainium/app_sources/vlc.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart';
@ -363,15 +364,23 @@ abstract class AppSource {
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,
{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) {
var req = Request('GET', Uri.parse(url));
req.followRedirects = followRedirects;
if (requestHeaders != null) {
req.headers.addAll(requestHeaders!);
req.headers.addAll(requestHeaders);
}
return Response.fromStream(await Client().send(req));
} else {
@ -512,6 +521,20 @@ regExValidator(String? value) {
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 {
// Add more source classes here so they are available via the service
List<AppSource> get sources => [
@ -519,15 +542,16 @@ class SourceProvider {
GitLab(),
Codeberg(),
FDroid(),
IzzyOnDroid(),
FDroidRepo(),
Jenkins(),
IzzyOnDroid(),
SourceForge(),
SourceHut(),
Aptoide(),
APKMirror(),
APKPure(),
Aptoide(),
Uptodown(),
APKMirror(),
HuaweiAppGallery(),
Jenkins(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(),
Signal(),

View File

@ -46,10 +46,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e
url: "https://pub.dev"
source: hosted
version: "3.3.8"
version: "3.3.9"
args:
dependency: transitive
description:
@ -198,10 +198,10 @@ packages:
dependency: "direct main"
description:
name: dynamic_color
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c"
url: "https://pub.dev"
source: hosted
version: "1.6.6"
version: "1.6.7"
easy_localization:
dependency: "direct main"
description:
@ -320,10 +320,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330
sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f
url: "https://pub.dev"
source: hosted
version: "0.6.17+2"
version: "0.6.17+3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -538,18 +538,18 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70
url: "https://pub.dev"
source: hosted
version: "10.4.3"
version: "11.0.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3
sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f
url: "https://pub.dev"
source: hosted
version: "10.3.4"
version: "11.0.1"
permission_handler_apple:
dependency: transitive
description:
@ -562,10 +562,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
url: "https://pub.dev"
source: hosted
version: "3.11.3"
version: "3.11.5"
permission_handler_windows:
dependency: transitive
description:
@ -686,6 +686,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description: flutter
@ -879,18 +887,18 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa"
sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
url: "https://pub.dev"
source: hosted
version: "3.9.5"
version: "3.10.1"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b"
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "2.6.0"
webview_flutter_wkwebview:
dependency: transitive
description:

View File

@ -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
# 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.
version: 0.14.10+202 # When changing this, update the tag in main() accordingly
version: 0.14.17+209 # When changing this, update the tag in main() accordingly
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.
# To automatically upgrade your package dependencies to the latest versions
@ -46,7 +46,7 @@ dependencies:
html: ^0.15.0
shared_preferences: ^2.0.15
url_launcher: ^6.1.5
permission_handler: ^10.0.0
permission_handler: ^11.0.0
fluttertoast: ^8.0.9
device_info_plus: ^9.0.0
file_picker: ^5.2.10
@ -65,6 +65,7 @@ dependencies:
flutter_archive: ^5.0.0
hsluv: ^1.1.3
connectivity_plus: ^4.0.2
shared_storage: ^0.8.0
dev_dependencies:
flutter_test: