mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
227 Commits
v0.13.21-b
...
v0.14.17-b
Author | SHA1 | Date | |
---|---|---|---|
845eaab73e | |||
298ff190ec | |||
734a548bc3 | |||
591973d97f | |||
a2f57ecd66 | |||
13dcfb479d | |||
3328d80130 | |||
f9df3ac0e2 | |||
61d66e80d8 | |||
93e0360116 | |||
2cec5f7934 | |||
8d4a09b007 | |||
6d8d2fa99d | |||
59ecd55530 | |||
77fcc89e37 | |||
7d63da4d9c | |||
8eea0bae99 | |||
f5dcd8f776 | |||
e1f2baeeda | |||
84d8ff8b0b | |||
9862c22fdb | |||
8c82f53dd2 | |||
5cf4c21ce3 | |||
4951f62d4a | |||
4c0c4b7010 | |||
96051e614c | |||
a2e494b2ba | |||
2d5a9bec84 | |||
7d9571cfdd | |||
acc6a780fa | |||
0e36d42a06 | |||
5cfddd807a | |||
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 | |||
69680f8680 | |||
1a616aacb7 | |||
52d19f267e | |||
75a8dc9ee4 | |||
8c63920ef6 | |||
dda5619cdc | |||
9cc25fd931 | |||
f5a9eb4295 | |||
ebe256e482 | |||
e215585a64 | |||
8b01fc03ec | |||
9af2c8370d | |||
01f9003b8d | |||
73a3c7eb71 | |||
370ec1432e | |||
4b63c124bb | |||
3d7df604b1 | |||
59d1d275bc | |||
f7f0332b13 | |||
917c5f5083 | |||
370cf05615 | |||
101d892c7f | |||
db2faf6511 | |||
3a83d1bf79 | |||
070e309aa6 | |||
11ca7c3f7d | |||
7a0acaefb3 | |||
486f3664ef | |||
387b165314 | |||
4ba3f7996e | |||
7ce41dfb67 | |||
efd55c8d36 | |||
71543c64d4 | |||
408c1a541f | |||
8f739d9e0b | |||
bd8f608ee6 | |||
551643b11c | |||
075ecae540 | |||
dcb8807fa7 | |||
77ec2df31c | |||
d92e812554 | |||
0a6e1f9cc6 | |||
f3c3680382 | |||
f7e783a556 | |||
fd1b72563d | |||
5a94ef82dd | |||
3ad46b7e21 | |||
1e94d71665 | |||
0899a576ff | |||
db2476f3a5 | |||
8ba182870d | |||
d81085a9e8 | |||
ae92a459b7 | |||
e101c434d5 | |||
9686d0f0ca | |||
1dd0392b78 | |||
5e184d733b | |||
ef0b20887b | |||
5cb4bd998b | |||
57f499c6a5 | |||
c2ae6e19f9 | |||
16104fde03 | |||
13e10692b1 | |||
7fc93f23c0 | |||
d6ddf87365 | |||
05172744cd | |||
2504ae24fc | |||
5e41d5762b | |||
57d44c972f | |||
be0b57ac00 | |||
862bb2b276 | |||
5eac851f80 | |||
0deab8296f | |||
6785708661 | |||
5307fd0901 | |||
788c4c7917 | |||
3f6d96289d | |||
5cfd80e510 | |||
9eb32ae55a | |||
82e08150ab | |||
e956ee9254 | |||
bb4f34317b | |||
d08ff3fbae | |||
f5479ec82f | |||
3eb25c4060 | |||
03bb1ad9a6 | |||
b59d3e77f9 | |||
7c41692d5f | |||
ce89d456e1 | |||
a0c48fcca6 | |||
d0cba6d6bc | |||
6baf6ccf4b | |||
a2571e61a8 | |||
f61824ff0d | |||
734a1aeb01 | |||
5269aad90d | |||
eaeee188eb | |||
8c850e06ca | |||
0f754a8da8 | |||
81c4d4f393 | |||
5317aee18d | |||
b66eeba3b5 | |||
522ff1ddf7 | |||
7ef9c43ee3 | |||
05ac76e3e9 | |||
4838402797 | |||
0de12c7c07 | |||
0dadd8bffe | |||
75a0cb1189 | |||
a52d936b4d | |||
cfd04dc602 | |||
7622e63975 | |||
3e7172b9d1 | |||
7aa0294447 | |||
5b89c4d293 | |||
3bb991f57f | |||
1dd3aa0e8a | |||
189cecbc37 | |||
9680ba08e9 | |||
dcf5bd37ca | |||
d6a4b0a96d | |||
5f8638d349 | |||
1de274df39 | |||
3df801c54e | |||
d473cb49c5 | |||
2a5118a2cf | |||
347c56da55 | |||
8073723e1f | |||
c8e90a755d | |||
aeb0a5d8ea | |||
09fe7f3ecd | |||
a549411589 | |||
f426b5e118 | |||
12a8ef5e31 | |||
8210279a4c | |||
999d13b80d | |||
0573c0b270 | |||
5a36a7980c | |||
6bf9b5297f | |||
fdc6b0ff00 | |||
73c20a53d2 | |||
16ae8d8e4d | |||
3f8cfae64e | |||
6861a71efb | |||
2a603f410f | |||
6d22788f92 | |||
e36e6bbaca | |||
ac3a13ed73 | |||
202f7df5cb | |||
1b902b1a18 | |||
1765d399c8 | |||
5dd79707f1 | |||
14755134bf | |||
cccde7e135 | |||
3dafd643c0 | |||
76f8cd4102 | |||
a8bfb03f58 | |||
10ead4f3e0 | |||
af5a6857ba |
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something isn't working right.
|
||||
title: ''
|
||||
labels: bug, to check
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Prerequisites**
|
||||
<!-- Please ensure your request is not part of an existing issue. -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Tap on '....'
|
||||
3. Scroll down to '....'
|
||||
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. -->
|
||||
|
||||
**Please complete the following information:**
|
||||
- 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. -->
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new Source, setting, or other feature.
|
||||
title: ''
|
||||
labels: enhancement, to check
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Prerequisites**
|
||||
<!-- 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.
|
||||
|
||||
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. -->
|
||||
|
||||
**Describe alternatives you've considered (if applicable)**
|
||||
<!-- 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). -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
27
README.md
27
README.md
@ -2,30 +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:
|
||||
- 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/)
|
||||
- [Mullvad](https://mullvad.net/en/)
|
||||
- [Signal](https://signal.org/)
|
||||
- [SourceForge](https://sourceforge.net/)
|
||||
- [SourceHut](https://git.sr.ht/)
|
||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||
- Other - General:
|
||||
- [APKPure](https://apkpure.com/)
|
||||
- Third Party F-Droid Repos
|
||||
- [Aptoide](https://aptoide.com/)
|
||||
- [Uptodown](https://uptodown.com/)
|
||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
||||
- Jenkins Jobs
|
||||
- [Steam](https://store.steampowered.com/mobile)
|
||||
- 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)
|
||||
- [VLC](https://www.videolan.org/vlc/download-android.html)
|
||||
- [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 (if multiple, the last file alphabetically is picked)
|
||||
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
|
||||
|
||||
## Installation
|
||||
|
||||
@ -33,8 +39,9 @@ Currently supported App sources:
|
||||
alt="Get it on GitHub"
|
||||
height="80">](https://github.com/ImranR98/Obtainium/releases)
|
||||
|
||||
[PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index)
|
||||
|
||||
## Limitations
|
||||
- Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin.
|
||||
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.
|
||||
|
||||
## Screenshots
|
||||
|
@ -49,7 +49,6 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "dev.imranr.obtainium"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
|
@ -62,6 +62,7 @@
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
@ -70,4 +71,5 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"/>
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
</manifest>
|
313
assets/translations/br.json
Normal file
313
assets/translations/br.json
Normal file
@ -0,0 +1,313 @@
|
||||
{
|
||||
"invalidURLForSource": "URL {} inválida",
|
||||
"noReleaseFound": "Não foi possivel encontrar uma versão adequada",
|
||||
"noVersionFound": "Não foi possivel encontrar uma versão lançada",
|
||||
"urlMatchesNoSource": "URL não corresponde a uma fonte conhecida",
|
||||
"cantInstallOlderVersion": "Não pode instalar uma versão anterior de um App",
|
||||
"appIdMismatch": "ID do pacote baixado não é igual ao ID do App instalado",
|
||||
"functionNotImplemented": "Esta classe não implementou essa função",
|
||||
"placeholder": "Espaço Reservado",
|
||||
"someErrors": "Alguns Erros Ocorreram",
|
||||
"unexpectedError": "Erro Inesperado",
|
||||
"ok": "Ok",
|
||||
"and": "e",
|
||||
"githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)",
|
||||
"includePrereleases": "Incluir pré-lançamentos",
|
||||
"fallbackToOlderReleases": "Retornar para versões anteriores",
|
||||
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
|
||||
"invalidRegEx": "Expressão Regular Inválida",
|
||||
"noDescription": "Sem descrição",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"requiredInBrackets": "(Necessário)",
|
||||
"dropdownNoOptsError": "ERRO: O DROPDOWN DEVE TER PELO MENOS UMA OPÇÃO",
|
||||
"colour": "Cor",
|
||||
"githubStarredRepos": "Favoritados no GitHub",
|
||||
"uname": "Nome de usuário",
|
||||
"wrongArgNum": "Número de argumentos errado",
|
||||
"xIsTrackOnly": "{} é 'Apenas Seguir'",
|
||||
"source": "Fonte",
|
||||
"app": "App",
|
||||
"appsFromSourceAreTrackOnly": "Os apps desta fonte são 'Apenas Seguir'.",
|
||||
"youPickedTrackOnly": "Você selecionou a opção 'Apenas Seguir'.",
|
||||
"trackOnlyAppDescription": "Esse App vai ser seguido por atualizações, mais o Obtainium não poderá baixa-lo ou instala-lo.",
|
||||
"cancelled": "Cancelado",
|
||||
"appAlreadyAdded": "App já adicionado",
|
||||
"alreadyUpToDateQuestion": "App já atualizado?",
|
||||
"addApp": "Adicionar App",
|
||||
"appSourceURL": "URL de origem do App",
|
||||
"error": "Erro",
|
||||
"add": "Adicionar",
|
||||
"searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
|
||||
"search": "Procurar",
|
||||
"additionalOptsFor": "Opções Adicionais para {}",
|
||||
"supportedSources": "Fontes Compatíveis",
|
||||
"trackOnlyInBrackets": "(Apenas Seguir)",
|
||||
"searchableInBrackets": "(Pesquisável)",
|
||||
"appsString": "Apps",
|
||||
"noApps": "Sem Apps",
|
||||
"noAppsForFilter": "Sem Apps para Filtrar",
|
||||
"byX": "Por {}",
|
||||
"percentProgress": "Progresso: {}%",
|
||||
"pleaseWait": "Por Favor Espere",
|
||||
"updateAvailable": "Atualização Disponível",
|
||||
"estimateInBracketsShort": "(Aprox.)",
|
||||
"notInstalled": "Não Instalado",
|
||||
"estimateInBrackets": "(Aproximado)",
|
||||
"selectAll": "Selecionar All",
|
||||
"deselectN": "Deselecionar {}",
|
||||
"xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.",
|
||||
"removeSelectedAppsQuestion": "Remover Apps Selecionados?",
|
||||
"removeSelectedApps": "Remover Apps Selecionados",
|
||||
"updateX": "Atualizar {}",
|
||||
"installX": "Instalar {}",
|
||||
"markXTrackOnlyAsUpdated": "Marcar {}\n(Apenas Seguir)\ncomo Atualizado",
|
||||
"changeX": "Mudar {}",
|
||||
"installUpdateApps": "Instalar/Atualizar Apps",
|
||||
"installUpdateSelectedApps": "Instalar/Atualizar Apps Selecionados",
|
||||
"markXSelectedAppsAsUpdated": "Marcar {} Apps Delecionados como Atualizados?",
|
||||
"no": "Não",
|
||||
"yes": "Sim",
|
||||
"markSelectedAppsUpdated": "Marcar Apps Selecionados como Atualizados",
|
||||
"pinToTop": "Fixar no topo",
|
||||
"unpinFromTop": "Desafixar do topo",
|
||||
"resetInstallStatusForSelectedAppsQuestion": "Reiniciar Status de Instalação para Apps Seleciondos?",
|
||||
"installStatusOfXWillBeResetExplanation": "O status de instalação de qualquer app selecionado sera reiniciado.\n\nIsso pode ajudar quando uma versão de um App mostrada no Obtainium é incorreta devido a falhas ao atualizar ou outros problemas.",
|
||||
"shareSelectedAppURLs": "Compartilhar URLs de Apps Selecionados",
|
||||
"resetInstallStatus": "Reiniciar Status de Instalação",
|
||||
"more": "Mais",
|
||||
"removeOutdatedFilter": "Remover Filtro de Apps Desatualizados",
|
||||
"showOutdatedOnly": "Mostrar Apenas Apps Desatualizados",
|
||||
"filter": "Filtro",
|
||||
"filterActive": "Filtro *",
|
||||
"filterApps": "Filtrar Apps",
|
||||
"appName": "Nome do App",
|
||||
"author": "Autor",
|
||||
"upToDateApps": "Apps Atualizados",
|
||||
"nonInstalledApps": "Apps Não Instalados",
|
||||
"importExport": "Importar/Exportar",
|
||||
"settings": "Configurações",
|
||||
"exportedTo": "Exportado para {}",
|
||||
"obtainiumExport": "Exportar Obtainium",
|
||||
"invalidInput": "Input Inválido",
|
||||
"importedX": "Importado {}",
|
||||
"obtainiumImport": "Importar Obtainium",
|
||||
"importFromURLList": "Importar de Lista de URLs",
|
||||
"searchQuery": "Pesquisa",
|
||||
"appURLList": "Lista de URLs de Apps",
|
||||
"line": "Linha",
|
||||
"searchX": "Pesquisa {}",
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"importX": "Importar {}",
|
||||
"importedAppsIdDisclaimer": "Apps Importados podem ser mostrados incorretamente como \"Não Instalado\".\nPara consertar, reinstale-os usando o Obtainium.\nIsso não deve afetar dados do App.\n\nAfeta apenas métodos de importação de URL e de terceiros.",
|
||||
"importErrors": "Erros de Importação",
|
||||
"importedXOfYApps": "{} de {} Apps importados.",
|
||||
"followingURLsHadErrors": "As seguintes URLs apresentaram erros:",
|
||||
"okay": "Ok",
|
||||
"selectURL": "Selecionar URL",
|
||||
"selectURLs": "Selecionar URLs",
|
||||
"pick": "Escolher",
|
||||
"theme": "Tema",
|
||||
"dark": "Escuro",
|
||||
"light": "Claro",
|
||||
"followSystem": "Seguir o Sistema",
|
||||
"obtainium": "Obtainium",
|
||||
"materialYou": "Material You",
|
||||
"useBlackTheme": "Usar tema preto completamente escuro",
|
||||
"appSortBy": "Classificar App por",
|
||||
"authorName": "Autor/Nome",
|
||||
"nameAuthor": "Nome/Autor",
|
||||
"asAdded": "Como Adicionado",
|
||||
"appSortOrder": "Ordem de classificação de Apps",
|
||||
"ascending": "Ascendente",
|
||||
"descending": "Descendente",
|
||||
"bgUpdateCheckInterval": "Intervalo de verificação de atualizações em segundo plano",
|
||||
"neverManualOnly": "Nunca - Apenas Manual",
|
||||
"appearance": "Aparência",
|
||||
"showWebInAppView": "Mostrar páginas da internet em App view",
|
||||
"pinUpdates": "Fixar atualizações no topo da visão de Apps",
|
||||
"updates": "Atualizações",
|
||||
"sourceSpecific": "Específico a fonte",
|
||||
"appSource": "Fonte de Apps",
|
||||
"noLogs": "Sem Logs",
|
||||
"appLogs": "Logs do App",
|
||||
"close": "Fechar",
|
||||
"share": "Compartilhar",
|
||||
"appNotFound": "App não encontrado",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Selecionar um APK",
|
||||
"appHasMoreThanOnePackage": "{} tem mais de um pacote:",
|
||||
"deviceSupportsXArch": "Seu dispositivo suporta a arquitetura de CPU {}.",
|
||||
"deviceSupportsFollowingArchs": "Seu dispositivo suporta as seguintes arquiteturas de CPU:",
|
||||
"warning": "Aviso",
|
||||
"sourceIsXButPackageFromYPrompt": "A Fonte do App é '{}' mais o pacote lançado vem de '{}'. Continuar?",
|
||||
"updatesAvailable": "Atualizações Disponíveis",
|
||||
"updatesAvailableNotifDescription": "Notifica o usuário quando atualizações estão disponíveis um ou mais Apps seguidos pelo Obtainium",
|
||||
"noNewUpdates": "Sem novas atualizações.",
|
||||
"xHasAnUpdate": "{} tem uma atualização.",
|
||||
"appsUpdated": "Apps Atualizados",
|
||||
"appsUpdatedNotifDescription": "Notifica o usuário quando atualizações para um ou mais Apps foram aplicadas em segundo plano",
|
||||
"xWasUpdatedToY": "{} foi atualizado para {}.",
|
||||
"errorCheckingUpdates": "Erro ao Procurar por Atualizações",
|
||||
"errorCheckingUpdatesNotifDescription": "Uma notificação que mostra quando a checagem por atualizações em segundo plano falha",
|
||||
"appsRemoved": "Apps Removidos",
|
||||
"appsRemovedNotifDescription": "Notifica o usuário quando um ou mais Apps foram removidos devido a erros ao carregá-los",
|
||||
"xWasRemovedDueToErrorY": "{} foi removido devido a este erro: {}",
|
||||
"completeAppInstallation": "Instalação completa do App",
|
||||
"obtainiumMustBeOpenToInstallApps": "Obtainium deve estar aberto para instalar Apps",
|
||||
"completeAppInstallationNotifDescription": "Pede ao usuário que retorne ao Obtainium para finalizar a instalação de um App",
|
||||
"checkingForUpdates": "Checando por Atualizações",
|
||||
"checkingForUpdatesNotifDescription": "Notificação transiente que aparece quando checando por atualizações",
|
||||
"pleaseAllowInstallPerm": "Por favor, permita o Obtainium instalar Apps",
|
||||
"trackOnly": "Apenas Seguir",
|
||||
"errorWithHttpStatusCode": "Erro {}",
|
||||
"versionCorrectionDisabled": "Correção de versão desativada (plugin parece não funcionar)",
|
||||
"unknown": "Desconhecido",
|
||||
"none": "Nenhum",
|
||||
"never": "Nunca",
|
||||
"latestVersionX": "Última versão: {}",
|
||||
"installedVersionX": "Versão Instalada: {}",
|
||||
"lastUpdateCheckX": "Última Checagem por Atualização: {}",
|
||||
"remove": "Remover",
|
||||
"yesMarkUpdated": "Sim, Marcar como Atualizado",
|
||||
"fdroid": "F-Droid Official",
|
||||
"appIdOrName": "ID do App ou Nome",
|
||||
"appId": "ID do App",
|
||||
"appWithIdOrNameNotFound": "Nenhum App foi encontrado com esse ID ou nome",
|
||||
"reposHaveMultipleApps": "Repositórios podem conter multiplos Apps",
|
||||
"fdroidThirdPartyRepo": "Repositórios de terceiros F-Droid",
|
||||
"steam": "Steam",
|
||||
"steamMobile": "Steam Mobile",
|
||||
"steamChat": "Steam Chat",
|
||||
"install": "Instalar",
|
||||
"markInstalled": "Marcar Instalado",
|
||||
"update": "Atualizar",
|
||||
"markUpdated": "Marcar Atualizado",
|
||||
"additionalOptions": "Opções Adicionais",
|
||||
"disableVersionDetection": "Desativar Detecção de Versão",
|
||||
"noVersionDetectionExplanation": "Essa opção deve apenas ser usada por Apps onde detecção de versão não funciona corretamente.",
|
||||
"downloadingX": "Baixando {}",
|
||||
"downloadNotifDescription": "Notifica o usuário do progresso ao baixar um App",
|
||||
"noAPKFound": "APK não encontrado",
|
||||
"noVersionDetection": "Sem Detecção de versão",
|
||||
"categorize": "Categorizar",
|
||||
"categories": "Categorias",
|
||||
"category": "Categoria",
|
||||
"noCategory": "Sem Categoria",
|
||||
"noCategories": "Sem Categoria",
|
||||
"deleteCategoriesQuestion": "Deletar Categorias?",
|
||||
"categoryDeleteWarning": "Todos os Apps em categorias removidas serão descategorizados.",
|
||||
"addCategory": "Adicionar Categoria",
|
||||
"label": "Etiqueta",
|
||||
"language": "Linguagem",
|
||||
"copiedToClipboard": "Copiado para a área de transferência",
|
||||
"storagePermissionDenied": "Permição ao armazenamento negada",
|
||||
"selectedCategorizeWarning": "Isso vai substituir qualquer confirução de categoria para os Apps selecionados.",
|
||||
"filterAPKsByRegEx": "Filtrar APKs por Expressão Regular",
|
||||
"removeFromObtainium": "Remover do Obtainium",
|
||||
"uninstallFromDevice": "Desinstalar do dispositivo",
|
||||
"onlyWorksWithNonVersionDetectApps": "Apenas funciona para Apps com detecção de versão desativada.",
|
||||
"releaseDateAsVersion": "Usar Data de Lançamento como Versão",
|
||||
"releaseDateAsVersionExplanation": "Esta opção só deve ser usada para aplicativos onde a detecção de versão não funciona corretamente, mas há uma data de lançamento disponível.",
|
||||
"changes": "Mudanças",
|
||||
"releaseDate": "Data de Lançamento",
|
||||
"importFromURLsInFile": "Importar de URLs em Arquivo (como OPML)",
|
||||
"versionDetection": "Detecção de Versão",
|
||||
"standardVersionDetection": "Detecção de versão padrão",
|
||||
"groupByCategory": "Agroupar por Categoria",
|
||||
"autoApkFilterByArch": "Tente filtrar APKs por arquitetura de CPU, se possível",
|
||||
"overrideSource": "Substituir Fonte",
|
||||
"dontShowAgain": "Não mostrar isso novamente",
|
||||
"dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Seguir'",
|
||||
"dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK",
|
||||
"moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps",
|
||||
"gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
|
||||
"about": "Sobre",
|
||||
"requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)",
|
||||
"checkOnStart": "Checar por atualizações ao iniciar ",
|
||||
"tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte",
|
||||
"removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente",
|
||||
"pickHighestVersionCode": "Auto-selecionar o maior numero de versão do APK",
|
||||
"checkUpdateOnDetailPage": "Checar por atualizações ao abrir a pagina de detalhes de um App",
|
||||
"disablePageTransitions": "Desativar animações de transição de pagina",
|
||||
"reversePageTransitions": "Reverter animações de transição de pagina",
|
||||
"minStarCount": "Contagem Minima de Estrelas",
|
||||
"addInfoBelow": "Adicionar essa informação abaixo.",
|
||||
"addInfoInSettings": "Adicionar essa informação nas configurações.",
|
||||
"githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
|
||||
"gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.",
|
||||
"sortByFileNamesNotLinks": "Classifique por nomes de arquivos em vez de links completos",
|
||||
"filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular",
|
||||
"customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
|
||||
"appsPossiblyUpdated": "Tentativas de atualização de Apps",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifica o usuário de que atualizações de um ou mais Apps foram potencialmente aplicadas em segundo plano",
|
||||
"xWasPossiblyUpdatedToY": "{} pode ter sido atualizado para {}.",
|
||||
"enableBackgroundUpdates": "Ativar atualizações em segundo plano",
|
||||
"backgroundUpdateReqsExplanation": "Atualizações em segundo plano podem não ser possíveis para todos os Apps.",
|
||||
"backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.",
|
||||
"verifyLatestTag": "Verifique a 'ultima' etiqueta",
|
||||
"intermediateLinkRegex": "Filtre por um Link 'Intermediário' para Visitar Primeiro",
|
||||
"intermediateLinkNotFound": "Link intermediário não encontrado",
|
||||
"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?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Muitas solicitações (taxa limitada) - tente novamente em {} minuto",
|
||||
"other": "Muitas solicitações (taxa limitada) - tente novamente em {} minutos"
|
||||
},
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "A verificação de atualizações em segundo plano encontrou um {}, agendada uma nova verificação em {} minuto",
|
||||
"other": "A verificação de atualizações em segundo plano encontrou um {}, agendada uma nova verificação em {} minutos"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "A verificação de atualizações em segundo plano encontrou {} atualização, o usuário sera notificado caso necessário",
|
||||
"other": "A verificação de atualizações em segundo plano encontrou {} atualizações, o usuário sera notificado caso necessário"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} App",
|
||||
"other": "{} Apps"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} URL",
|
||||
"other": "{} URLs"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} Minuto",
|
||||
"other": "{} Minutos"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} Hora",
|
||||
"other": "{} Horas"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} Dia",
|
||||
"other": "{} Dias"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Limpo {n} log (antes = {antes}, depois = {depois})",
|
||||
"other": "Limpados {n} logs (antes = {antes}, depois = {depois})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} e 1 outro app tem atualizações.",
|
||||
"other": "{} e {} outros apps tem atualizações."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} e 1 outro app foi atualizado.",
|
||||
"other": "{} e {} outros apps foram atualizados."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} e 1 outro app pode ter sido atualizado.",
|
||||
"other": "{} e {} outros apps podem ter sido atualizados."
|
||||
}
|
||||
}
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Neočekivana greška",
|
||||
"ok": "Dobro",
|
||||
"and": "i",
|
||||
"startedBgUpdateTask": "Započeo je pozadinski zadatak provjere ažuriranja",
|
||||
"bgUpdateIgnoreAfterIs": "ignoreAfter pozadinskog zadataka je {}",
|
||||
"startedActualBGUpdateCheck": "Započela je stvarna provjera ažuriranja",
|
||||
"bgUpdateTaskFinished": "Završen zadatak provjere ažuriranja",
|
||||
"firstRun": "Ovo je prvi put da pokrećete Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Podešavanje intervala ažuriranja na {}",
|
||||
"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",
|
||||
@ -49,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",
|
||||
@ -240,6 +232,29 @@
|
||||
"disablePageTransitions": "Ugasite animaciju prijelaza stranice",
|
||||
"reversePageTransitions": "Reverzne animacije prijelaza stranice",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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": "Želite li ukloniti aplikaciju?",
|
||||
"other": "Želite li ukloniti aplikacije?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} i još 1 aplikacija je ažurirana.",
|
||||
"other": "{} i još {} aplikacija je ažurirano."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Unerwarteter Fehler",
|
||||
"ok": "Okay",
|
||||
"and": "und",
|
||||
"startedBgUpdateTask": "Hintergrundaktualisierungsprüfung gestartet",
|
||||
"bgUpdateIgnoreAfterIs": "Hintergrundaktualisierung 'ignoreAfter' ist {}",
|
||||
"startedActualBGUpdateCheck": "Überprüfung der Hintergrundaktualisierung gestartet",
|
||||
"bgUpdateTaskFinished": "Hintergrundaktualisierungsprüfung abgeschlossen",
|
||||
"firstRun": "Dies ist der erste Start von Obtainium überhaupt",
|
||||
"settingUpdateCheckIntervalTo": "Aktualisierungsintervall auf {} stellen",
|
||||
"githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
|
||||
"githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
|
||||
"githubPATFormat": "Benutzername:Token",
|
||||
"includePrereleases": "Vorabversionen einbeziehen",
|
||||
"fallbackToOlderReleases": "Fallback auf ältere Versionen",
|
||||
"filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
|
||||
@ -36,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",
|
||||
@ -46,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",
|
||||
@ -78,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",
|
||||
@ -183,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",
|
||||
@ -229,17 +221,40 @@
|
||||
"dontShowTrackOnlyWarnings": "Warnung für 'Nur Nachverfolgen' nicht anzeigen",
|
||||
"dontShowAPKOriginWarnings": "Warnung für APK-Herkunft nicht anzeigen",
|
||||
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche and Better APK Discovery)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
|
||||
"about": "Über",
|
||||
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
|
||||
"checkOnStart": "Überprüfe einmalig beim Start",
|
||||
"tryInferAppIdFromCode": "Try inferring App ID from source code",
|
||||
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
|
||||
"pickHighestVersionCode": "Auto-select highest version code APK",
|
||||
"checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
|
||||
"removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
|
||||
"pickHighestVersionCode": "Automatische Auswahl des APK mit höchstem Versionscode",
|
||||
"checkUpdateOnDetailPage": "Nach Updates suchen, wenn eine App-Detailseite geöffnet wird",
|
||||
"disablePageTransitions": "Animationen für Seitenübergänge deaktivieren",
|
||||
"reversePageTransitions": "Umgekehrte Animationen für Seitenübergänge",
|
||||
"minStarCount": "Minimale Anzahl von Sternen",
|
||||
"addInfoBelow": "Fügen Sie diese Informationen unten hinzu.",
|
||||
"addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
|
||||
"githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
|
||||
"gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
|
||||
"sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links",
|
||||
"filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
|
||||
"customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
|
||||
"appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
|
||||
"appsPossiblyUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Updates für eine oder mehrere Apps möglicherweise im Hintergrund durchgeführt wurden",
|
||||
"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",
|
||||
"exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
|
||||
"bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
|
||||
"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?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.",
|
||||
"other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Unexpected Error",
|
||||
"ok": "Okay",
|
||||
"and": "and",
|
||||
"startedBgUpdateTask": "Started BG update check task",
|
||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||
"startedActualBGUpdateCheck": "Started actual BG update checking",
|
||||
"bgUpdateTaskFinished": "Finished BG update check task",
|
||||
"firstRun": "This is the first ever run of Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Setting update interval to {}",
|
||||
"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",
|
||||
@ -49,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",
|
||||
@ -240,6 +232,32 @@
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"enableBackgroundUpdates": "Enable background updates",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
|
||||
"intermediateLinkNotFound": "Intermediate link not found",
|
||||
"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?"
|
||||
@ -285,7 +303,11 @@
|
||||
"other": "{} and {} more apps have updates."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} and 1 more app were updated.",
|
||||
"one": "{} and 1 more app was updated.",
|
||||
"other": "{} and {} more apps were updated."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Error Inesperado",
|
||||
"ok": "Correcto",
|
||||
"and": "y",
|
||||
"startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano",
|
||||
"bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es {}",
|
||||
"startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano",
|
||||
"bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano",
|
||||
"firstRun": "Esta es la primera ejecución de Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}",
|
||||
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
|
||||
"githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
|
||||
"githubPATFormat": "nombre_de_usuario:token",
|
||||
"includePrereleases": "Incluir versiones preliminares",
|
||||
"fallbackToOlderReleases": "Retorceder a versiones previas",
|
||||
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
|
||||
@ -49,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",
|
||||
@ -240,6 +232,29 @@
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} y 1 aplicación más han sido actualizadas.",
|
||||
"other": "{} y {} aplicaciones más han sido actualizadas."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "خطای غیرمنتظره",
|
||||
"ok": "باشه",
|
||||
"and": "و",
|
||||
"startedBgUpdateTask": "شروع بررسی بروزرسانی BG",
|
||||
"bgUpdateIgnoreAfterIs": "نادیده گرفتن بروزرسانی BG بعد از {} است",
|
||||
"startedActualBGUpdateCheck": "بررسی بهروزرسانی واقعی BG آغاز شد",
|
||||
"bgUpdateTaskFinished": "کار بررسی بهروزرسانی BG تمام شد",
|
||||
"firstRun": "این اولین اجرای Obtainium است",
|
||||
"settingUpdateCheckIntervalTo": "تنظیم فاصله بهروزرسانی روی {}",
|
||||
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
|
||||
"githubPATHint": "PAT باید در این قالب باشد: username:token",
|
||||
"githubPATFormat": "username:token",
|
||||
"includePrereleases": "شامل نسخه های اولیه",
|
||||
"fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
|
||||
"filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
|
||||
@ -49,7 +41,7 @@
|
||||
"searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
|
||||
"search": "جستجو کردن",
|
||||
"additionalOptsFor": "گزینه های اضافی برای {}",
|
||||
"supportedSourcesBelow": "منابع پشتیبانی شده:",
|
||||
"supportedSources": "منابع پشتیبانی شده",
|
||||
"trackOnlyInBrackets": "«فقط ردیابی»",
|
||||
"searchableInBrackets": "(قابل جستجو)",
|
||||
"appsString": "برنامه ها",
|
||||
@ -229,17 +221,40 @@
|
||||
"dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید",
|
||||
"dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید",
|
||||
"moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
|
||||
"gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو را فعال می کند and Better APK Discovery)",
|
||||
"gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
|
||||
"about": "درباره",
|
||||
"requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
|
||||
"checkOnStart": "بررسی در شروع",
|
||||
"tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
|
||||
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
|
||||
"pickHighestVersionCode": "Auto-select highest version code APK",
|
||||
"checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
|
||||
"pickHighestVersionCode": "انتخاب خودکار بالاترین کد نسخه APK",
|
||||
"checkUpdateOnDetailPage": "برای باز کردن صفحه جزئیات برنامه، بهروزرسانیها را بررسی کنید",
|
||||
"disablePageTransitions": "غیرفعال کردن انیمیشن های انتقال صفحه",
|
||||
"reversePageTransitions": "انیمیشن های انتقال معکوس صفحه",
|
||||
"minStarCount": "حداقل تعداد ستاره",
|
||||
"addInfoBelow": "این اطلاعات را در زیر اضافه کنید",
|
||||
"addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
|
||||
"githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
|
||||
"gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
|
||||
"sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
|
||||
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
|
||||
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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": "برنامه ها حذف شوند؟"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} و 1 برنامه دیگر به روز شدند.",
|
||||
"other": "{} و {} برنامه دیگر به روز شدند."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Erreur inattendue",
|
||||
"ok": "Okay",
|
||||
"and": "et",
|
||||
"startedBgUpdateTask": "Démarrage de la tâche de vérification de mise à jour en arrière-plan",
|
||||
"bgUpdateIgnoreAfterIs": "Mise à jour en arrière-plan est ignoré après {}",
|
||||
"startedActualBGUpdateCheck": "Démarrage de la vérification de la mise à jour en arrière-plan",
|
||||
"bgUpdateTaskFinished": "Tâche de vérification de la mise à jour en arrière-plan terminée",
|
||||
"firstRun": "Il s'agit de la toute première exécution d'Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Définition de l'intervalle de mise à jour sur {}",
|
||||
"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",
|
||||
@ -45,11 +37,11 @@
|
||||
"addApp": "Ajouter une application",
|
||||
"appSourceURL": "URL de la source de l'application",
|
||||
"error": "Erreur",
|
||||
"add": "Ajoutée",
|
||||
"add": "Ajouter",
|
||||
"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",
|
||||
@ -240,6 +232,29 @@
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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 ?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} et 1 autre application ont été mises à jour.",
|
||||
"other": "{} et {} autres applications ont été mises à jour."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Váratlan hiba",
|
||||
"ok": "Oké",
|
||||
"and": "és",
|
||||
"startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva",
|
||||
"bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}",
|
||||
"startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése",
|
||||
"bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött",
|
||||
"firstRun": "Ez az Obtainium első futása",
|
||||
"settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}",
|
||||
"githubPATLabel": "GitHub személyes hozzáférési token (megnöveli a díjkorlátot)",
|
||||
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
|
||||
"githubPATFormat": "felhasználónév:token",
|
||||
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
|
||||
"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",
|
||||
@ -49,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",
|
||||
@ -96,10 +88,10 @@
|
||||
"importExport": "Import/Export",
|
||||
"settings": "Beállítások",
|
||||
"exportedTo": "Exportálva ide {}",
|
||||
"obtainiumExport": "Obtainium Export",
|
||||
"obtainiumExport": "Obtainium Adat Exportálás",
|
||||
"invalidInput": "Hibás bemenet",
|
||||
"importedX": "Importálva innen {}",
|
||||
"obtainiumImport": "Obtainium Import",
|
||||
"obtainiumImport": "Obtainium Adat Importálás",
|
||||
"importFromURLList": "Importálás URL listából",
|
||||
"searchQuery": "Keresési lekérdezés",
|
||||
"appURLList": "App URL lista",
|
||||
@ -139,8 +131,8 @@
|
||||
"appSource": "App forrás",
|
||||
"noLogs": "Nincsenek naplók",
|
||||
"appLogs": "App naplók",
|
||||
"close": "Bezár",
|
||||
"share": "Megoszt",
|
||||
"close": "Bezárás",
|
||||
"share": "Megosztás",
|
||||
"appNotFound": "App nem található",
|
||||
"obtainiumExportHyphenatedLowercase": "obtainium-export",
|
||||
"pickAnAPK": "Válasszon egy APK-t",
|
||||
@ -222,23 +214,46 @@
|
||||
"versionDetection": "Verzió érzékelés",
|
||||
"standardVersionDetection": "Alapért. verzió érzékelés",
|
||||
"groupByCategory": "Csoportosítás Kategória alapján",
|
||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
|
||||
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-okat",
|
||||
"overrideSource": "Forrás felülbírálása",
|
||||
"dontShowAgain": "Ne mutassa ezt újra",
|
||||
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
|
||||
"dontShowAPKOriginWarnings": "Ne jelenítsen meg az APK eredetére vonatkozó figyelmeztetéseket",
|
||||
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést and Better APK Discovery)",
|
||||
"gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
|
||||
"about": "Rólunk",
|
||||
"requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
|
||||
"checkOnStart": "Egyszer az indításkor",
|
||||
"checkOnStart": "Egyszer az alkalmazás indításakor is",
|
||||
"tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
|
||||
"removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása",
|
||||
"pickHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
|
||||
"checkUpdateOnDetailPage": "Frissítések keresése az app részleteit tartalmazó oldal megnyitásakor",
|
||||
"disablePageTransitions": "Lap áttűnési animációk tiltása",
|
||||
"disablePageTransitions": "Lap áttűnési animációk letiltása",
|
||||
"reversePageTransitions": "Fordított lap áttűnési animációk",
|
||||
"minStarCount": "Minimális csillag szám",
|
||||
"addInfoBelow": "Adja hozzá ezt az infót alább.",
|
||||
"addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
|
||||
"githubSourceNote": "A GitHub sebességkorlátozás elkerülhető API-kulcs használatával.",
|
||||
"gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.",
|
||||
"sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett",
|
||||
"filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
|
||||
"customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
|
||||
"appsPossiblyUpdated": "App frissítési kísérlet",
|
||||
"appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben",
|
||||
"xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.",
|
||||
"backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
|
||||
"backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
|
||||
"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": "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?"
|
||||
@ -281,10 +296,14 @@
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "A(z) {} és 1 további alkalmazás frissítéseket kapott.",
|
||||
"other": "{} és további {} alkalmazás frissítéseket kapott."
|
||||
"other": "{} és {} további alkalmazás frissítéseket kapott."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "A(z) {} és 1 további alkalmazás frissítve.",
|
||||
"other": "{} és további {} alkalmazás frissítve."
|
||||
"other": "{} és {} további alkalmazás frissítve."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} és 1 további alkalmazás is frissült.",
|
||||
"other": "{} és {} további alkalmazás is frissült."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Errore imprevisto",
|
||||
"ok": "Va bene",
|
||||
"and": "e",
|
||||
"startedBgUpdateTask": "Avviata l'attività di controllo degli aggiornamenti in secondo piano",
|
||||
"bgUpdateIgnoreAfterIs": "Il parametro di agg. in secondo piano 'ignoreAfter' è {}",
|
||||
"startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in secondo piano",
|
||||
"bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in secondo piano",
|
||||
"firstRun": "Questo è il primo avvio di sempre di Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}",
|
||||
"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",
|
||||
@ -49,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",
|
||||
@ -240,6 +232,29 @@
|
||||
"disablePageTransitions": "Disable page transition animations",
|
||||
"reversePageTransitions": "Reverse page transition animations",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"addInfoBelow": "Add this info below.",
|
||||
"addInfoInSettings": "Add this info in the Settings.",
|
||||
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
|
||||
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} e un'altra app sono state aggiornate.",
|
||||
"other": "{} e altre {} app sono state aggiornate."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,18 +11,10 @@
|
||||
"unexpectedError": "予期せぬエラーが発生しました",
|
||||
"ok": "OK",
|
||||
"and": "と",
|
||||
"startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始",
|
||||
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}",
|
||||
"startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始",
|
||||
"bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了",
|
||||
"firstRun": "これがObtainiumの最初の実行です",
|
||||
"settingUpdateCheckIntervalTo": "確認間隔を{}に設定する",
|
||||
"githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
|
||||
"githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
|
||||
"githubPATFormat": "ユーザー名:トークン",
|
||||
"includePrereleases": "プレリリースを含む",
|
||||
"fallbackToOlderReleases": "旧リリースへのフォールバック",
|
||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む",
|
||||
"filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
|
||||
"invalidRegEx": "無効な正規表現",
|
||||
"noDescription": "説明はありません",
|
||||
"cancel": "キャンセル",
|
||||
@ -49,7 +41,7 @@
|
||||
"searchSomeSourcesLabel": "検索 (一部ソースのみ)",
|
||||
"search": "検索",
|
||||
"additionalOptsFor": "{}の追加オプション",
|
||||
"supportedSourcesBelow": "対応するソース:",
|
||||
"supportedSources": "対応するソース",
|
||||
"trackOnlyInBrackets": "(追跡のみ)",
|
||||
"searchableInBrackets": "(検索可能)",
|
||||
"appsString": "アプリ",
|
||||
@ -88,7 +80,7 @@
|
||||
"showOutdatedOnly": "アップデートが存在するアプリのみ表示する",
|
||||
"filter": "フィルター",
|
||||
"filterActive": "フィルター *",
|
||||
"filterApps": "アプリを絞り込む",
|
||||
"filterApps": "アプリをフィルタリングする",
|
||||
"appName": "アプリ名",
|
||||
"author": "作者",
|
||||
"upToDateApps": "最新のアプリ",
|
||||
@ -152,7 +144,7 @@
|
||||
"updatesAvailable": "アップデートが利用可能",
|
||||
"updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する",
|
||||
"noNewUpdates": "新しいアップデートはありません",
|
||||
"xHasAnUpdate": "{} のアップデートが利用可能です",
|
||||
"xHasAnUpdate": "{} のアップデートが利用可能です。",
|
||||
"appsUpdated": "アプリをアップデートしました",
|
||||
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
|
||||
"xWasUpdatedToY": "{} が {} にアップデートされました",
|
||||
@ -211,7 +203,7 @@
|
||||
"copiedToClipboard": "クリップボードにコピーしました",
|
||||
"storagePermissionDenied": "ストレージ権限が拒否されました",
|
||||
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
|
||||
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
|
||||
"filterAPKsByRegEx": "正規表現でAPKをフィルタリングする",
|
||||
"removeFromObtainium": "Obtainiumから削除する",
|
||||
"uninstallFromDevice": "デバイスからアンインストールする",
|
||||
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
|
||||
@ -239,7 +231,33 @@
|
||||
"checkUpdateOnDetailPage": "アプリの詳細ページを開く際にアップデートを確認する",
|
||||
"disablePageTransitions": "ページ遷移アニメーションを無効化する",
|
||||
"reversePageTransitions": "ページ遷移アニメーションを反転する",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"minStarCount": "最小スター数",
|
||||
"addInfoBelow": "下部でこの情報を追加してください。",
|
||||
"addInfoInSettings": "設定でこの情報を追加してください。",
|
||||
"githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
|
||||
"gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
|
||||
"sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする",
|
||||
"filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
|
||||
"customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
|
||||
"appsPossiblyUpdated": "アプリのアップデートを試行",
|
||||
"appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する",
|
||||
"xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります。",
|
||||
"enableBackgroundUpdates": "バックグラウンドアップデートを有効化する",
|
||||
"backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
|
||||
"backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
|
||||
"verifyLatestTag": "'latest'タグを確認する",
|
||||
"intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
|
||||
"intermediateLinkNotFound": "中間リンクが見つかりませんでした",
|
||||
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
|
||||
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
|
||||
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
|
||||
"versionExtractionRegEx": "バージョン抽出の正規表現",
|
||||
"matchGroupToUse": "使用するマッチしたグループ",
|
||||
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
|
||||
"pickExportDir": "エクスポートディレクトリを選択",
|
||||
"autoExportOnChanges": "変更があった際に自動でエクスポートする",
|
||||
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
|
||||
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
|
||||
"removeAppQuestion": {
|
||||
"one": "アプリを削除しますか?",
|
||||
"other": "アプリを削除しますか?"
|
||||
@ -281,11 +299,15 @@
|
||||
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
|
||||
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です"
|
||||
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です。",
|
||||
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です。"
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} とさらに {} 個のアプリがアップデートされました",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされました"
|
||||
"one": "{} とさらに {} 個のアプリがアップデートされました。",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされました。"
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。",
|
||||
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。"
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@
|
||||
"okay": "Okej",
|
||||
"appId": "ID aplikacji",
|
||||
"bgUpdateGotErrorRetryInMinutes": {
|
||||
"one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min.",
|
||||
"other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min."
|
||||
"one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minutę",
|
||||
"few": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty",
|
||||
"many": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minut",
|
||||
"other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty"
|
||||
},
|
||||
"invalidURLForSource": "Nieprawidłowy adres URL aplikacji {}",
|
||||
"noReleaseFound": "Nie można znaleźć odpowiedniego wydania",
|
||||
@ -19,15 +21,7 @@
|
||||
"unexpectedError": "Nieoczekiwany błąd",
|
||||
"ok": "Okej",
|
||||
"and": "i",
|
||||
"startedBgUpdateTask": "Rozpoczęto zadanie sprawdzania aktualizacji w tle",
|
||||
"bgUpdateIgnoreAfterIs": "Parametr ignoreAfter aktualizacji w tle to {}",
|
||||
"startedActualBGUpdateCheck": "Rozpoczęto sprawdzanie aktualizacji w tle",
|
||||
"bgUpdateTaskFinished": "Zakończono zadanie sprawdzania aktualizacji w tle",
|
||||
"firstRun": "Jest to pierwsze uruchomienie Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Ustawianie interwału aktualizacji na {}",
|
||||
"githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
|
||||
"githubPATHint": "Wymagany format: 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",
|
||||
@ -56,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",
|
||||
@ -243,49 +237,103 @@
|
||||
"checkUpdateOnDetailPage": "Sprawdzaj aktualizacje podczas otwierania strony szczegółów aplikacji",
|
||||
"disablePageTransitions": "Wyłącz animacje przejścia między stronami",
|
||||
"reversePageTransitions": "Odwróć animacje przejścia pomiędzy stronami",
|
||||
"minStarCount": "Minimum Star Count",
|
||||
"minStarCount": "Minimalna ilość gwiazdek",
|
||||
"addInfoBelow": "Dodaj tę informację poniżej.",
|
||||
"addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
|
||||
"githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
|
||||
"gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.",
|
||||
"sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków",
|
||||
"filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
|
||||
"customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
|
||||
"appsPossiblyUpdated": "Informuj o próbach aktualizacji",
|
||||
"appsPossiblyUpdatedNotifDescription": "Powiadamiaj o potencjalnym zastosowaniu w tle aktualizacji jednej lub większej ilości aplikacji",
|
||||
"xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.",
|
||||
"backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
|
||||
"backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
|
||||
"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": "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?",
|
||||
"many": "Usunąć aplikacje?",
|
||||
"other": "Usunąć aplikacje?"
|
||||
},
|
||||
"tooManyRequestsTryAgainInMinutes": {
|
||||
"one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min.",
|
||||
"other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min."
|
||||
"one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minutę",
|
||||
"few": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty",
|
||||
"many": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minut",
|
||||
"other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "Podczas sprawdzania aktualizacji w tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"other": "Podczas sprawdzania aktualizacji w tle znaleziono {} akt. - w razie potrzeby użytkownik zostanie o tym powiadomiony"
|
||||
"one": "W tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"few": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"many": "W tle znaleziono {} aktualizacji - w razie potrzeby użytkownik zostanie o tym powiadomiony",
|
||||
"other": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} aplik.",
|
||||
"other": "{} aplik."
|
||||
"one": "{} apkę",
|
||||
"few": "{} apki",
|
||||
"many": "{} apek",
|
||||
"other": "{} apki"
|
||||
},
|
||||
"url": {
|
||||
"one": "{} adres URL",
|
||||
"other": "{} adr. URL"
|
||||
"few": "{} adresy URL",
|
||||
"many": "{} adresów URL",
|
||||
"other": "{} adresy URL"
|
||||
},
|
||||
"minute": {
|
||||
"one": "{} min.",
|
||||
"other": "{} min."
|
||||
"one": "{} minuta",
|
||||
"few": "{} minuty",
|
||||
"many": "{} minut",
|
||||
"other": "{} minuty"
|
||||
},
|
||||
"hour": {
|
||||
"one": "{} godz.",
|
||||
"other": "{} godz."
|
||||
"one": "{} godzina",
|
||||
"few": "{} godziny",
|
||||
"many": "{} godzin",
|
||||
"other": "{} godziny"
|
||||
},
|
||||
"day": {
|
||||
"one": "{} dzień",
|
||||
"few": "{} dni",
|
||||
"many": "{} dni",
|
||||
"other": "{} dni"
|
||||
},
|
||||
"clearedNLogsBeforeXAfterY": {
|
||||
"one": "Wyczyszczono {n} log (przed = {before}, po = {after})",
|
||||
"other": "Wyczyszczono logi: {n} (przed = {before}, po = {after})"
|
||||
"few": "Wyczyszczono {n} logi (przed = {before}, po = {after})",
|
||||
"many": "Wyczyszczono {n} logów (przed = {before}, po = {after})",
|
||||
"other": "Wyczyszczono {n} logi (przed = {before}, po = {after})"
|
||||
},
|
||||
"xAndNMoreUpdatesAvailable": {
|
||||
"one": "{} i jeszcze 1 aplikacja mają aktualizacje.",
|
||||
"other": "{} i {} aplik. otrzymało aktualizacje."
|
||||
"one": "{} i 1 inna apka mają aktualizacje.",
|
||||
"few": "{} i {} inne apki mają aktualizacje.",
|
||||
"many": "{} i {} innych apek ma aktualizacje.",
|
||||
"other": "{} i {} inne apki mają aktualizacje."
|
||||
},
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "Zaktualizowano {} i jeszcze 1 aplikację.",
|
||||
"other": "Zaktualizowano {} i {} aplik."
|
||||
}
|
||||
"one": "Zaktualizowano {} i 1 inną apkę.",
|
||||
"few": "{} i {} inne apki zostały zaktualizowane.",
|
||||
"many": "{} i {} innych apek zostało zaktualizowanych.",
|
||||
"other": "{} i {} inne apki zostały zaktualizowane."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} i 1 inna apka mogły zostać zaktualizowane.",
|
||||
"few": "{} i {} inne apki mogły zostać zaktualizowane.",
|
||||
"many": "{} i {} innych apek mogło zostać zaktualizowanych.",
|
||||
"other": "{} i {} inne apki mogły zostać zaktualizowane."
|
||||
},
|
||||
"enableBackgroundUpdates": "Włącz aktualizacje w tle",
|
||||
"intermediateLinkRegex": "Filtr linków \"pośrednich\" do odwiedzenia w pierwszej kolejności",
|
||||
"intermediateLinkNotFound": "Nie znaleziono linku pośredniego"
|
||||
}
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "Неожиданная ошибка",
|
||||
"ok": "Окей",
|
||||
"and": "и",
|
||||
"startedBgUpdateTask": "Запущена задача фоновой проверки обновлений",
|
||||
"bgUpdateIgnoreAfterIs": "Параметр игнорирования фоновых обновлений: {}",
|
||||
"startedActualBGUpdateCheck": "Запущена фактическая проверка фоновых обновлений",
|
||||
"bgUpdateTaskFinished": "Завершена задача фоновой проверки обновлений",
|
||||
"firstRun": "Это первый запуск Obtainium",
|
||||
"settingUpdateCheckIntervalTo": "Установка интервала проверки обновлений: {}",
|
||||
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
|
||||
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
|
||||
"githubPATFormat": "имя_пользователя:токен",
|
||||
"includePrereleases": "Включить предварительные релизы",
|
||||
"fallbackToOlderReleases": "Откатиться к более старым версиям",
|
||||
"filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
|
||||
@ -49,7 +41,7 @@
|
||||
"searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
|
||||
"search": "Поиск",
|
||||
"additionalOptsFor": "Дополнительные опции для {}",
|
||||
"supportedSourcesBelow": "Поддерживаемые источники:",
|
||||
"supportedSources": "Поддерживаемые источники",
|
||||
"trackOnlyInBrackets": "(Только для отслеживания)",
|
||||
"searchableInBrackets": "(Поиск)",
|
||||
"appsString": "Приложения",
|
||||
@ -240,6 +232,29 @@
|
||||
"disablePageTransitions": "Отключить анимацию перехода между страницами",
|
||||
"reversePageTransitions": "Реверс анимации перехода между страницами",
|
||||
"minStarCount": "Минимальное количество звёзд",
|
||||
"addInfoBelow": "Добавьте эту информацию ниже.",
|
||||
"addInfoInSettings": "Добавьте эту информацию в Настройки.",
|
||||
"githubSourceNote": "Лимит запросов GitHub можно обойти, используя ключ API.",
|
||||
"gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.",
|
||||
"sortByFileNamesNotLinks": "Sort by file names instead of full links",
|
||||
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
|
||||
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
|
||||
"appsPossiblyUpdated": "App Updates Attempted",
|
||||
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
|
||||
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
|
||||
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
|
||||
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
|
||||
"verifyLatestTag": "Verify the 'latest' tag",
|
||||
"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": "Удалить приложения?"
|
||||
@ -287,5 +302,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} и еще 1 приложение были обновлены.",
|
||||
"other": "{} и еще {} приложений были обновлены."
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} and 1 more app may have been updated.",
|
||||
"other": "{} and {} more apps may have been updated."
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,7 @@
|
||||
"unexpectedError": "意外错误",
|
||||
"ok": "好的",
|
||||
"and": "和",
|
||||
"startedBgUpdateTask": "后台更新检查任务已启动",
|
||||
"bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}",
|
||||
"startedActualBGUpdateCheck": "开始后台更新检查",
|
||||
"bgUpdateTaskFinished": "后台更新检查任务已完成",
|
||||
"firstRun": "这是 Obtainium 首次启动",
|
||||
"settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}",
|
||||
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
|
||||
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
|
||||
"githubPATFormat": "username:token",
|
||||
"includePrereleases": "包含预发行版",
|
||||
"fallbackToOlderReleases": "将旧发行版作为备选",
|
||||
"filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
|
||||
@ -46,10 +38,10 @@
|
||||
"appSourceURL": "来源 URL",
|
||||
"error": "错误",
|
||||
"add": "添加",
|
||||
"searchSomeSourcesLabel": "搜索(仅部分来源)",
|
||||
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
|
||||
"search": "搜索",
|
||||
"additionalOptsFor": "{} 的更多选项",
|
||||
"supportedSourcesBelow": "支持的来源:",
|
||||
"supportedSources": "支持的来源",
|
||||
"trackOnlyInBrackets": "(仅追踪)",
|
||||
"searchableInBrackets": "(可搜索)",
|
||||
"appsString": "应用列表",
|
||||
@ -107,7 +99,7 @@
|
||||
"searchX": "搜索 {}",
|
||||
"noResults": "无结果",
|
||||
"importX": "导入 {}",
|
||||
"importedAppsIdDisclaimer": "导入的应用可能错误地显示为“未安装”。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
|
||||
"importedAppsIdDisclaimer": "导入的应用可能会错误地显示为“未安装”状态。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
|
||||
"importErrors": "导入错误",
|
||||
"importedXOfYApps": "已导入 {} 中的 {} 个应用。",
|
||||
"followingURLsHadErrors": "下列 URL 存在错误:",
|
||||
@ -150,16 +142,16 @@
|
||||
"warning": "警告",
|
||||
"sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?",
|
||||
"updatesAvailable": "更新可用",
|
||||
"updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发出通知",
|
||||
"updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发送通知",
|
||||
"noNewUpdates": "全部应用已是最新。",
|
||||
"xHasAnUpdate": "{} 可以更新了。",
|
||||
"appsUpdated": "应用已更新",
|
||||
"appsUpdatedNotifDescription": "当应用在后台安装更新时发出通知",
|
||||
"appsUpdatedNotifDescription": "当应用在后台安装更新时发送通知",
|
||||
"xWasUpdatedToY": "{} 已更新至 {}。",
|
||||
"errorCheckingUpdates": "检查更新出错",
|
||||
"errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知",
|
||||
"appsRemoved": "应用已删除",
|
||||
"appsRemovedNotifDescription": "当应用因加载出错而被删除时发出通知",
|
||||
"appsRemovedNotifDescription": "当应用因加载出错而被删除时发送通知",
|
||||
"xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}",
|
||||
"completeAppInstallation": "完成应用安装",
|
||||
"obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用",
|
||||
@ -180,7 +172,7 @@
|
||||
"yesMarkUpdated": "是,标记为已更新",
|
||||
"fdroid": "F-Droid 官方存储库",
|
||||
"appIdOrName": "应用 ID 或名称",
|
||||
"appId": "App ID",
|
||||
"appId": "应用 ID",
|
||||
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
|
||||
"reposHaveMultipleApps": "存储库中可能包含多个应用",
|
||||
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
|
||||
@ -229,7 +221,7 @@
|
||||
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
|
||||
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
|
||||
"moveNonInstalledAppsToBottom": "将未安装应用置底",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌\n(用于搜索应用 and Better APK Discovery)",
|
||||
"gitlabPATLabel": "GitLab 个人访问令牌\n(启用搜索功能并增强 APK 发现)",
|
||||
"about": "相关文档",
|
||||
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
|
||||
"checkOnStart": "启动时进行一次检查",
|
||||
@ -240,6 +232,30 @@
|
||||
"disablePageTransitions": "禁用页面过渡动画效果",
|
||||
"reversePageTransitions": "反转页面过渡动画效果",
|
||||
"minStarCount": "最小星标数",
|
||||
"addInfoBelow": "在下方添加此凭据。",
|
||||
"addInfoInSettings": "在“设置”中添加此凭据。",
|
||||
"githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
|
||||
"gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
|
||||
"sortByFileNamesNotLinks": "使用文件名代替链接进行排序",
|
||||
"filterReleaseNotesByRegEx": "使用正则表达式筛选发行说明",
|
||||
"customLinkFilterRegex": "使用正则表达式自定义链接筛选(默认模式为“.apk$”)",
|
||||
"appsPossiblyUpdated": "已尝试更新应用",
|
||||
"appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知",
|
||||
"xWasPossiblyUpdatedToY": "已尝试将 {} 更新至 {}。",
|
||||
"enableBackgroundUpdates": "启用后台更新",
|
||||
"backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
|
||||
"backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
|
||||
"verifyLatestTag": "验证“Latest”标签",
|
||||
"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": "是否删除应用?"
|
||||
@ -253,8 +269,8 @@
|
||||
"other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试"
|
||||
},
|
||||
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
|
||||
"one": "后台检查发现 {} 个应用更新 - 如有需要将发出通知",
|
||||
"other": "后台检查发现 {} 个应用更新 - 如有需要将发出通知"
|
||||
"one": "后台检查发现 {} 个应用更新 - 如有需要将发送通知",
|
||||
"other": "后台检查发现 {} 个应用更新 - 如有需要将发送通知"
|
||||
},
|
||||
"apps": {
|
||||
"one": "{} 个应用",
|
||||
@ -287,5 +303,9 @@
|
||||
"xAndNMoreUpdatesInstalled": {
|
||||
"one": "{} 和另外 1 个应用已更新。",
|
||||
"other": "{} 和另外 {} 个应用已更新。"
|
||||
},
|
||||
"xAndNMoreUpdatesPossiblyInstalled": {
|
||||
"one": "{} 和另外 1 个应用已尝试更新。",
|
||||
"other": "{} 和另外 {} 个应用已尝试更新。"
|
||||
}
|
||||
}
|
||||
|
20
build.sh
Executable file
20
build.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Convenience script
|
||||
|
||||
CURR_DIR="$(pwd)"
|
||||
trap "cd "$CURR_DIR"" EXIT
|
||||
|
||||
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
|
||||
rsync -r ./build/app/outputs/flutter-apk/ ~/Downloads/Obtainium-build/ # Dropoff in Downloads to allow for drag-drop into Flatpak Firefox
|
||||
cd ~/Downloads/Obtainium-build/ # Make zips just in case (for in-comment uploads)
|
||||
for apk in *.apk; do
|
||||
PREFIX="$(echo "$apk" | head -c -5)"
|
||||
zip "$PREFIX" "$PREFIX"*
|
||||
done
|
||||
mkdir -p zips
|
||||
mv *.zip zips/
|
@ -25,12 +25,16 @@ class APKCombo extends AppSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> get requestHeaders => {
|
||||
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');
|
||||
|
@ -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);
|
||||
}
|
||||
|
76
lib/app_sources/aptoide.dart
Normal file
76
lib/app_sources/aptoide.dart
Normal file
@ -0,0 +1,76 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Aptoide extends AppSource {
|
||||
Aptoide() {
|
||||
host = 'aptoide.com';
|
||||
name = tr('Aptoide');
|
||||
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);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return (await getAppDetailsJSON(standardUrl))['package'];
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async {
|
||||
var res = await sourceRequest(standardUrl);
|
||||
if (res.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
|
||||
String? id;
|
||||
if (idMatch != null) {
|
||||
id = res.body.substring(idMatch.start + 12, idMatch.end);
|
||||
} else {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
var res2 =
|
||||
await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id');
|
||||
if (res2.statusCode != 200) {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
return jsonDecode(res2.body)?['nodes']?['meta']?['data'];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
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) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
DateTime? relDate;
|
||||
if (dateStr != null) {
|
||||
relDate = DateTime.parse(dateStr);
|
||||
}
|
||||
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||
releaseDate: relDate);
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -9,28 +7,8 @@ class Codeberg extends AppSource {
|
||||
Codeberg() {
|
||||
host = 'codeberg.org';
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [];
|
||||
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormSwitch('includePrereleases',
|
||||
label: tr('includePrereleases'), defaultValue: false)
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('fallbackToOlderReleases',
|
||||
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('filterReleaseTitlesByRegEx',
|
||||
label: tr('filterReleaseTitlesByRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
]
|
||||
];
|
||||
additionalSourceAppSpecificSettingFormItems =
|
||||
gh.additionalSourceAppSpecificSettingFormItems;
|
||||
|
||||
canSearch = true;
|
||||
searchQuerySettingFormItems = gh.searchQuerySettingFormItems;
|
||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -11,6 +12,26 @@ class FDroid extends AppSource {
|
||||
host = 'f-droid.org';
|
||||
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'))
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
@ -37,21 +58,74 @@ class FDroid extends AppSource {
|
||||
}
|
||||
|
||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
Response res, String apkUrlPrefix, String standardUrl) {
|
||||
Response res, String apkUrlPrefix, String standardUrl,
|
||||
{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();
|
||||
}
|
||||
List<String> apkUrls = releases
|
||||
.where((element) => element['versionName'] == latestVersion)
|
||||
}
|
||||
// Default to the highest version
|
||||
version ??= releases[0]['versionName'];
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
@ -68,7 +142,17 @@ class FDroid extends AppSource {
|
||||
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest('https://$host/api/v1/packages/$appId'),
|
||||
'https://$host/repo/$appId',
|
||||
standardUrl);
|
||||
standardUrl,
|
||||
autoSelectHighestVersionCode:
|
||||
additionalSettings['autoSelectHighestVersionCode'] == true,
|
||||
trySelectingSuggestedVersionCode:
|
||||
additionalSettings['trySelectingSuggestedVersionCode'] == true,
|
||||
filterVersionsByRegEx:
|
||||
(additionalSettings['filterVersionsByRegEx'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? additionalSettings['filterVersionsByRegEx']
|
||||
: null);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -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';
|
||||
@ -16,25 +17,11 @@ class GitHub extends AppSource {
|
||||
host = 'github.com';
|
||||
appIdInferIsOptional = true;
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [
|
||||
sourceConfigSettingFormItems = [
|
||||
GeneratedFormTextField('github-creds',
|
||||
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(
|
||||
@ -75,6 +62,20 @@ class GitHub extends AppSource {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('filterReleaseNotesByRegEx',
|
||||
label: tr('filterReleaseNotesByRegEx'),
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormSwitch('verifyLatestTag',
|
||||
label: tr('verifyLatestTag'), defaultValue: false)
|
||||
]
|
||||
];
|
||||
|
||||
@ -107,7 +108,7 @@ class GitHub extends AppSource {
|
||||
for (var path in possibleBuildGradleLocations) {
|
||||
try {
|
||||
var res = await sourceRequest(
|
||||
'${await convertStandardUrlToAPIUrl(standardUrl)}/contents/$path');
|
||||
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path');
|
||||
if (res.statusCode == 200) {
|
||||
try {
|
||||
var body = jsonDecode(res.body);
|
||||
@ -155,19 +156,57 @@ class GitHub extends AppSource {
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
Future<String> getCredentialPrefixIfAny() async {
|
||||
SettingsProvider settingsProvider = SettingsProvider();
|
||||
await settingsProvider.initializeSettings();
|
||||
String? creds = settingsProvider
|
||||
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
|
||||
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
||||
@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> getAPIHost() async =>
|
||||
'https://${await getCredentialPrefixIfAny()}api.$host';
|
||||
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'];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> convertStandardUrlToAPIUrl(String standardUrl) async =>
|
||||
'${await getAPIHost()}/repos${standardUrl.substring('https://$host'.length)}';
|
||||
@override
|
||||
Future<String?> getSourceNote() async {
|
||||
if (!hostChanged && (await getTokenIfAny({})) == null) {
|
||||
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
|
||||
'https://api.$host';
|
||||
|
||||
Future<String> convertStandardUrlToAPIUrl(
|
||||
String standardUrl, Map<String, dynamic> additionalSettings) async =>
|
||||
'${await getAPIHost(additionalSettings)}/repos${standardUrl.substring('https://$host'.length)}';
|
||||
|
||||
@override
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||
@ -185,6 +224,27 @@ class GitHub extends AppSource {
|
||||
true
|
||||
? additionalSettings['filterReleaseTitlesByRegEx']
|
||||
: null;
|
||||
String? regexNotesFilter =
|
||||
(additionalSettings['filterReleaseNotesByRegEx'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? additionalSettings['filterReleaseNotesByRegEx']
|
||||
: null;
|
||||
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
|
||||
String? latestTag;
|
||||
if (verifyLatestTag) {
|
||||
var temp = requestUrl.split('?');
|
||||
Response res = await sourceRequest(
|
||||
'${temp[0]}/latest${temp.length > 1 ? '?${temp.sublist(1).join('?')}' : ''}');
|
||||
if (res.statusCode != 200) {
|
||||
if (onHttpErrorCode != null) {
|
||||
onHttpErrorCode(res);
|
||||
}
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
var jsres = jsonDecode(res.body);
|
||||
latestTag = jsres['tag_name'] ?? jsres['name'];
|
||||
}
|
||||
Response res = await sourceRequest(requestUrl);
|
||||
if (res.statusCode == 200) {
|
||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||
@ -192,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'))
|
||||
@ -231,6 +292,17 @@ class GitHub extends AppSource {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (latestTag != null &&
|
||||
releases.isNotEmpty &&
|
||||
latestTag !=
|
||||
(releases[releases.length - 1]['tag_name'] ??
|
||||
releases[0]['name'])) {
|
||||
var ind = releases.indexWhere(
|
||||
(element) => latestTag == (element['tag_name'] ?? element['name']));
|
||||
if (ind >= 0) {
|
||||
releases.add(releases.removeAt(ind));
|
||||
}
|
||||
}
|
||||
releases = releases.reversed.toList();
|
||||
dynamic targetRelease;
|
||||
var prerrelsSkipped = 0;
|
||||
@ -253,6 +325,11 @@ class GitHub extends AppSource {
|
||||
!RegExp(regexFilter).hasMatch(nameToFilter.trim())) {
|
||||
continue;
|
||||
}
|
||||
if (regexNotesFilter != null &&
|
||||
!RegExp(regexNotesFilter)
|
||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||
continue;
|
||||
}
|
||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||
continue;
|
||||
@ -311,7 +388,7 @@ class GitHub extends AppSource {
|
||||
) async {
|
||||
return await getLatestAPKDetailsCommon2(standardUrl, additionalSettings,
|
||||
(bool useTagUrl) async {
|
||||
return '${await convertStandardUrlToAPIUrl(standardUrl)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||
return '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/${useTagUrl ? 'tags' : 'releases'}?per_page=100';
|
||||
}, (Response res) {
|
||||
rateLimitErrorCheck(res);
|
||||
});
|
||||
@ -360,7 +437,7 @@ class GitHub extends AppSource {
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
return searchCommon(
|
||||
query,
|
||||
'${await getAPIHost()}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||
'${await getAPIHost({})}/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100',
|
||||
'items', onHttpErrorCode: (Response res) {
|
||||
rateLimitErrorCheck(res);
|
||||
}, querySettings: querySettings);
|
||||
|
@ -16,7 +16,7 @@ class GitLab extends AppSource {
|
||||
host = 'gitlab.com';
|
||||
canSearch = true;
|
||||
|
||||
additionalSourceSpecificSettingFormItems = [
|
||||
sourceConfigSettingFormItems = [
|
||||
GeneratedFormTextField('gitlab-creds',
|
||||
label: tr('gitlabPATLabel'),
|
||||
password: true,
|
||||
@ -60,18 +60,27 @@ class GitLab extends AppSource {
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
Future<String?> getPATIfAny() async {
|
||||
Future<String?> getPATIfAny(Map<String, dynamic> additionalSettings) async {
|
||||
SettingsProvider settingsProvider = SettingsProvider();
|
||||
await settingsProvider.initializeSettings();
|
||||
String? creds = settingsProvider
|
||||
.getSettingString(additionalSourceSpecificSettingFormItems[0].key);
|
||||
var sourceConfig =
|
||||
await getSourceConfigValues(additionalSettings, settingsProvider);
|
||||
String? creds = sourceConfig['gitlab-creds'];
|
||||
return creds != null && creds.isNotEmpty ? creds : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getSourceNote() async {
|
||||
if ((await getPATIfAny({})) == null) {
|
||||
return '${tr('gitlabSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<String>>> search(String query,
|
||||
{Map<String, dynamic> querySettings = const {}}) async {
|
||||
String? PAT = await getPATIfAny();
|
||||
String? PAT = await getPATIfAny({});
|
||||
if (PAT == null) {
|
||||
throw CredsNeededError(name);
|
||||
}
|
||||
@ -103,7 +112,7 @@ class GitLab extends AppSource {
|
||||
) async {
|
||||
bool fallbackToOlderReleases =
|
||||
additionalSettings['fallbackToOlderReleases'] == true;
|
||||
String? PAT = await getPATIfAny();
|
||||
String? PAT = await getPATIfAny(hostChanged ? additionalSettings : {});
|
||||
Iterable<APKDetails> apkDetailsList = [];
|
||||
if (PAT != null) {
|
||||
var names = GitHub().getAppNames(standardUrl);
|
||||
|
@ -1,6 +1,8 @@
|
||||
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';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -17,7 +19,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
|
||||
.toList();
|
||||
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
|
||||
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';
|
||||
} else {
|
||||
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
|
||||
@ -85,12 +87,64 @@ bool _isNumeric(String s) {
|
||||
}
|
||||
|
||||
class HTML extends AppSource {
|
||||
HTML() {
|
||||
additionalSourceAppSpecificSettingFormItems = [
|
||||
[
|
||||
GeneratedFormSwitch('sortByFileNamesNotLinks',
|
||||
label: tr('sortByFileNamesNotLinks'))
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('customLinkFilterRegex',
|
||||
label: tr('customLinkFilterRegex'),
|
||||
hint: 'download/(.*/)?(android|apk|mobile)',
|
||||
required: false,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
return regExValidator(value);
|
||||
}
|
||||
])
|
||||
],
|
||||
[
|
||||
GeneratedFormTextField('intermediateLinkRegex',
|
||||
label: tr('intermediateLinkRegex'),
|
||||
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',
|
||||
disableStandard: true, disableRelDate: true);
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement requestHeaders choice, hardcoded for now
|
||||
Map<String, String>? get requestHeaders => {
|
||||
Future<Map<String, String>?> getRequestHeaders(
|
||||
{Map<String, dynamic> additionalSettings = const <String, dynamic>{},
|
||||
bool forAPKDownload = false}) async {
|
||||
return {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
|
||||
"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) {
|
||||
@ -105,14 +159,44 @@ class HTML extends AppSource {
|
||||
var uri = Uri.parse(standardUrl);
|
||||
Response res = await sourceRequest(standardUrl);
|
||||
if (res.statusCode == 200) {
|
||||
List<String> links = parse(res.body)
|
||||
var html = parse(res.body);
|
||||
List<String> allLinks = html
|
||||
.querySelectorAll('a')
|
||||
.map((element) => element.attributes['href'] ?? '')
|
||||
.toList();
|
||||
List<String> links = [];
|
||||
if ((additionalSettings['intermediateLinkRegex'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true) {
|
||||
var reg = RegExp(additionalSettings['intermediateLinkRegex']);
|
||||
links = allLinks.where((element) => reg.hasMatch(element)).toList();
|
||||
links.sort((a, b) => compareAlphaNumeric(a, b));
|
||||
if (links.isEmpty) {
|
||||
throw ObtainiumError(tr('intermediateLinkNotFound'));
|
||||
}
|
||||
Map<String, dynamic> additionalSettingsTemp =
|
||||
Map.from(additionalSettings);
|
||||
additionalSettingsTemp['intermediateLinkRegex'] = null;
|
||||
return getLatestAPKDetails(
|
||||
ensureAbsoluteUrl(links.last, uri), additionalSettingsTemp);
|
||||
}
|
||||
if ((additionalSettings['customLinkFilterRegex'] as String?)
|
||||
?.isNotEmpty ==
|
||||
true) {
|
||||
var reg = RegExp(additionalSettings['customLinkFilterRegex']);
|
||||
links = allLinks.where((element) => reg.hasMatch(element)).toList();
|
||||
} else {
|
||||
links = allLinks
|
||||
.where((element) =>
|
||||
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
links.sort((a, b) => compareAlphaNumeric(a, b));
|
||||
if (additionalSettings['apkFilterRegEx'] != null) {
|
||||
}
|
||||
links.sort((a, b) => additionalSettings['sortByFileNamesNotLinks'] == true
|
||||
? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last,
|
||||
b.split('/').where((e) => e.isNotEmpty).last)
|
||||
: compareAlphaNumeric(a, b));
|
||||
if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty ==
|
||||
true) {
|
||||
var reg = RegExp(additionalSettings['apkFilterRegEx']);
|
||||
links = links.where((element) => reg.hasMatch(element)).toList();
|
||||
}
|
||||
@ -120,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);
|
||||
|
91
lib/app_sources/huaweiappgallery.dart
Normal file
91
lib/app_sources/huaweiappgallery.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class HuaweiAppGallery extends AppSource {
|
||||
HuaweiAppGallery() {
|
||||
name = 'Huawei AppGallery';
|
||||
host = 'appgallery.huawei.com';
|
||||
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
||||
disableStandard: true);
|
||||
}
|
||||
|
||||
@override
|
||||
String sourceSpecificStandardizeURL(String url) {
|
||||
RegExp standardUrlRegEx = RegExp('^https?://$host/app/[^/]+');
|
||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||
if (match == null) {
|
||||
throw InvalidURLError(name);
|
||||
}
|
||||
return url.substring(0, match.end);
|
||||
}
|
||||
|
||||
getDlUrl(String standardUrl) =>
|
||||
'https://${host!.replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}';
|
||||
|
||||
requestAppdlRedirect(String dlUrl) async {
|
||||
Response res = await sourceRequest(dlUrl, followRedirects: false);
|
||||
if (res.statusCode == 200 ||
|
||||
res.statusCode == 302 ||
|
||||
res.statusCode == 304) {
|
||||
return res;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
appIdFromRedirectDlUrl(String redirectDlUrl) {
|
||||
var parts = redirectDlUrl
|
||||
.split('?')[0]
|
||||
.split('/')
|
||||
.last
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList();
|
||||
parts.removeAt(0);
|
||||
parts.removeAt(0);
|
||||
return parts.reversed.join('.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
String dlUrl = getDlUrl(standardUrl);
|
||||
Response res = await requestAppdlRedirect(dlUrl);
|
||||
return res.headers['location'] != null
|
||||
? appIdFromRedirectDlUrl(res.headers['location']!)
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String dlUrl = getDlUrl(standardUrl);
|
||||
Response res = await requestAppdlRedirect(dlUrl);
|
||||
if (res.headers['location'] == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String appId = appIdFromRedirectDlUrl(res.headers['location']!);
|
||||
var relDateStr =
|
||||
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1];
|
||||
var relDateStrAdj = relDateStr?.split('');
|
||||
var tempLen = relDateStrAdj?.length ?? 0;
|
||||
var i = 2;
|
||||
while (i < tempLen) {
|
||||
relDateStrAdj?.insert((i + i ~/ 2 - 1), '-');
|
||||
i += 2;
|
||||
}
|
||||
var relDate = relDateStrAdj == null
|
||||
? null
|
||||
: DateFormat('yy-MM-dd-HH-mm').parse(relDateStrAdj.join(''));
|
||||
if (relDateStr == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return APKDetails(
|
||||
relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId),
|
||||
releaseDate: relDate);
|
||||
}
|
||||
}
|
@ -3,8 +3,13 @@ import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class IzzyOnDroid extends AppSource {
|
||||
late FDroid fd;
|
||||
|
||||
IzzyOnDroid() {
|
||||
host = 'android.izzysoft.de';
|
||||
fd = FDroid();
|
||||
additionalSourceAppSpecificSettingFormItems =
|
||||
fd.additionalSourceAppSpecificSettingFormItems;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -20,7 +25,7 @@ class IzzyOnDroid extends AppSource {
|
||||
@override
|
||||
Future<String?> tryInferringAppId(String standardUrl,
|
||||
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||
return FDroid().tryInferringAppId(standardUrl);
|
||||
return fd.tryInferringAppId(standardUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -29,10 +34,15 @@ class IzzyOnDroid extends AppSource {
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
String? appId = await tryInferringAppId(standardUrl);
|
||||
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||
await sourceRequest(
|
||||
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
|
||||
'https://android.izzysoft.de/frepo/$appId',
|
||||
standardUrl);
|
||||
standardUrl,
|
||||
autoSelectHighestVersionCode:
|
||||
additionalSettings['autoSelectHighestVersionCode'] == true,
|
||||
trySelectingSuggestedVersionCode:
|
||||
additionalSettings['trySelectingSuggestedVersionCode'] == true,
|
||||
filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
class Jenkins extends AppSource {
|
||||
Jenkins() {
|
||||
overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
|
||||
overrideVersionDetectionFormDefault('releaseDateAsVersion',
|
||||
disableStandard: true);
|
||||
}
|
||||
|
||||
String trimJobUrl(String url) {
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/providers/source_provider.dart';
|
||||
|
||||
@ -7,19 +9,46 @@ class VLC extends AppSource {
|
||||
VLC() {
|
||||
host = 'videolan.org';
|
||||
}
|
||||
get dwUrlBase => 'https://get.$host/vlc-android/';
|
||||
|
||||
@override
|
||||
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) {
|
||||
return 'https://$host';
|
||||
}
|
||||
|
||||
Future<String?> getLatestVersion(String standardUrl) async {
|
||||
Response res = await sourceRequest(dwUrlBase);
|
||||
if (res.statusCode == 200) {
|
||||
var dwLinks = parse(res.body)
|
||||
.querySelectorAll('a')
|
||||
.where((element) => element.attributes['href'] != 'last/')
|
||||
.map((e) => e.attributes['href']?.split('/')[0])
|
||||
.toList();
|
||||
String? version = dwLinks.isNotEmpty ? dwLinks.last : null;
|
||||
if (version == null) {
|
||||
throw NoVersionError();
|
||||
}
|
||||
return version;
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<APKDetails> getLatestAPKDetails(
|
||||
String standardUrl,
|
||||
Map<String, dynamic> additionalSettings,
|
||||
) async {
|
||||
Response res = await sourceRequest(
|
||||
'https://www.videolan.org/vlc/download-android.html');
|
||||
Response res = await get(
|
||||
Uri.parse('https://www.videolan.org/vlc/download-android.html'));
|
||||
if (res.statusCode == 200) {
|
||||
var dwUrlBase = 'get.videolan.org/vlc-android';
|
||||
var dwLinks = parse(res.body)
|
||||
@ -37,26 +66,43 @@ class VLC extends AppSource {
|
||||
throw NoVersionError();
|
||||
}
|
||||
String? targetUrl = 'https://$dwUrlBase/$version/';
|
||||
Response res2 = await sourceRequest(targetUrl);
|
||||
String mirrorDwBase =
|
||||
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
|
||||
List<String> apkUrls = [];
|
||||
if (res2.statusCode == 200) {
|
||||
apkUrls = parse(res2.body)
|
||||
.querySelectorAll('a')
|
||||
.map((e) => e.attributes['href'])
|
||||
.where((h) =>
|
||||
h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk'))
|
||||
.map((e) => mirrorDwBase + e!)
|
||||
var apkUrls = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64']
|
||||
.map((e) => '${targetUrl}VLC-Android-$version-$e.apk')
|
||||
.toList();
|
||||
} else {
|
||||
throw getObtainiumHttpError(res2);
|
||||
}
|
||||
|
||||
return APKDetails(
|
||||
version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC'));
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> apkUrlPrefetchModifier(
|
||||
String apkUrl, String standardUrl) async {
|
||||
Response res = await sourceRequest(apkUrl);
|
||||
if (res.statusCode == 200) {
|
||||
String? apkUrl =
|
||||
parse(res.body).querySelector('#alt_link')?.attributes['href'];
|
||||
if (apkUrl == null) {
|
||||
throw NoAPKError();
|
||||
}
|
||||
return apkUrl;
|
||||
} else if (res.statusCode == 500 &&
|
||||
res.body.toLowerCase().indexOf('mirror') > 0) {
|
||||
var html = parse(res.body);
|
||||
var err = '';
|
||||
html.body?.nodes.forEach((element) {
|
||||
if (element.text != null) {
|
||||
err += '${element.text}\n';
|
||||
}
|
||||
});
|
||||
err = err.trim();
|
||||
if (err.isEmpty) {
|
||||
err = tr('err');
|
||||
}
|
||||
throw ObtainiumError(err);
|
||||
} else {
|
||||
throw getObtainiumHttpError(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
@ -373,7 +377,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
as Map<String, MapEntry<int, bool>>?)
|
||||
?.values
|
||||
.where((e) => e.value)
|
||||
.length == 1
|
||||
.length ==
|
||||
1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IconButton(
|
||||
@ -382,14 +387,16 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
||||
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);
|
||||
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));
|
||||
temp.update(oldEntry.key,
|
||||
(old) => MapEntry(newColor, old.value));
|
||||
values[widget.items[r][e].key] = temp;
|
||||
someValueChanged();
|
||||
});
|
||||
|
@ -11,7 +11,8 @@ class GeneratedFormModal extends StatefulWidget {
|
||||
this.initValid = false,
|
||||
this.message = '',
|
||||
this.additionalWidgets = const [],
|
||||
this.singleNullReturnButton});
|
||||
this.singleNullReturnButton,
|
||||
this.primaryActionColour});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
@ -19,6 +20,7 @@ class GeneratedFormModal extends StatefulWidget {
|
||||
final bool initValid;
|
||||
final List<Widget> additionalWidgets;
|
||||
final String? singleNullReturnButton;
|
||||
final Color? primaryActionColour;
|
||||
|
||||
@override
|
||||
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||
@ -71,6 +73,10 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
||||
: widget.singleNullReturnButton!)),
|
||||
widget.singleNullReturnButton == null
|
||||
? TextButton(
|
||||
style: widget.primaryActionColour == null
|
||||
? null
|
||||
: TextButton.styleFrom(
|
||||
foregroundColor: widget.primaryActionColour),
|
||||
onPressed: !valid
|
||||
? null
|
||||
: () {
|
||||
|
111
lib/main.dart
111
lib/main.dart
@ -1,9 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/pages/home.dart';
|
||||
import 'package:obtainium/providers/apps_provider.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
@ -21,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.13.21';
|
||||
const String currentVersion = '0.14.17';
|
||||
const String currentReleaseTag =
|
||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||
|
||||
@ -40,6 +38,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [
|
||||
MapEntry(Locale('pl'), 'Polski'),
|
||||
MapEntry(Locale('ru'), 'Русский язык'),
|
||||
MapEntry(Locale('bs'), 'Bosanski'),
|
||||
// MapEntry(Locale('br'), 'Brasileiro'),
|
||||
];
|
||||
const fallbackLocale = Locale('en');
|
||||
const localeDir = 'assets/translations';
|
||||
@ -71,84 +70,6 @@ Future<void> loadTranslations() async {
|
||||
fallbackTranslations: controller.fallbackTranslations);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
await loadTranslations();
|
||||
|
||||
LogsProvider logs = LogsProvider();
|
||||
logs.add(tr('startedBgUpdateTask'));
|
||||
int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds'];
|
||||
await AndroidAlarmManager.initialize();
|
||||
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||
: null;
|
||||
logs.add(tr('bgUpdateIgnoreAfterIs', args: [ignoreAfter.toString()]));
|
||||
var notificationsProvider = NotificationsProvider();
|
||||
await notificationsProvider.notify(checkingUpdatesNotification);
|
||||
try {
|
||||
var appsProvider = AppsProvider();
|
||||
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
|
||||
await appsProvider.loadApps();
|
||||
List<String> existingUpdateIds =
|
||||
appsProvider.findExistingUpdates(installedOnly: true);
|
||||
DateTime nextIgnoreAfter = DateTime.now();
|
||||
String? err;
|
||||
try {
|
||||
logs.add(tr('startedActualBGUpdateCheck'));
|
||||
await appsProvider.checkUpdates(
|
||||
ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true);
|
||||
} catch (e) {
|
||||
if (e is RateLimitError || e is SocketException) {
|
||||
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||
logs.add(plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes,
|
||||
args: [e.toString(), remainingMinutes.toString()]));
|
||||
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||
});
|
||||
} else {
|
||||
err = e.toString();
|
||||
}
|
||||
}
|
||||
List<App> newUpdates = appsProvider
|
||||
.findExistingUpdates(installedOnly: true)
|
||||
.where((id) => !existingUpdateIds.contains(id))
|
||||
.map((e) => appsProvider.apps[e]!.app)
|
||||
.toList();
|
||||
|
||||
// TODO: This silent update code doesn't work yet
|
||||
// List<String> silentlyUpdated = await appsProvider
|
||||
// .downloadAndInstallLatestApp(
|
||||
// [...newUpdates.map((e) => e.id), ...existingUpdateIds], null);
|
||||
// if (silentlyUpdated.isNotEmpty) {
|
||||
// newUpdates = newUpdates
|
||||
// .where((element) => !silentlyUpdated.contains(element.id))
|
||||
// .toList();
|
||||
// notificationsProvider.notify(
|
||||
// SilentUpdateNotification(
|
||||
// silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()),
|
||||
// cancelExisting: true);
|
||||
// }
|
||||
logs.add(
|
||||
plural('bgCheckFoundUpdatesWillNotifyIfNeeded', newUpdates.length));
|
||||
if (newUpdates.isNotEmpty) {
|
||||
notificationsProvider.notify(UpdateNotification(newUpdates));
|
||||
}
|
||||
if (err != null) {
|
||||
throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
notificationsProvider
|
||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||
} finally {
|
||||
logs.add(tr('bgUpdateTaskFinished'));
|
||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
@ -206,7 +127,7 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
} else {
|
||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||
if (isFirstRun) {
|
||||
logs.add(tr('firstRun'));
|
||||
logs.add('This is the first ever run of Obtainium.');
|
||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||
Permission.notification.request();
|
||||
appsProvider.saveApps([
|
||||
@ -233,23 +154,29 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
settingsProvider.resetLocaleSafe(context);
|
||||
}
|
||||
// Register the background update task according to the user's setting
|
||||
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
||||
if (existingUpdateInterval != -1) {
|
||||
logs.add(tr('settingUpdateCheckIntervalTo',
|
||||
args: [settingsProvider.updateInterval.toString()]));
|
||||
}
|
||||
existingUpdateInterval = settingsProvider.updateInterval;
|
||||
if (existingUpdateInterval == 0) {
|
||||
var actualUpdateInterval = settingsProvider.updateInterval;
|
||||
if (existingUpdateInterval != actualUpdateInterval) {
|
||||
if (actualUpdateInterval == 0) {
|
||||
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
|
||||
} else {
|
||||
var settingChanged = existingUpdateInterval != -1;
|
||||
var lastCheckWasTooLongAgo = actualUpdateInterval != 0 &&
|
||||
settingsProvider.lastBGCheckTime
|
||||
.add(Duration(minutes: actualUpdateInterval + 60))
|
||||
.isBefore(DateTime.now());
|
||||
if (settingChanged || lastCheckWasTooLongAgo) {
|
||||
logs.add(
|
||||
'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).');
|
||||
AndroidAlarmManager.periodic(
|
||||
Duration(minutes: existingUpdateInterval),
|
||||
Duration(minutes: actualUpdateInterval),
|
||||
bgUpdateCheckAlarmId,
|
||||
bgUpdateCheck,
|
||||
rescheduleOnReboot: true,
|
||||
wakeup: true);
|
||||
}
|
||||
}
|
||||
existingUpdateInterval = actualUpdateInterval;
|
||||
}
|
||||
}
|
||||
|
||||
return DynamicColorBuilder(
|
||||
@ -293,7 +220,9 @@ class _ObtainiumState extends State<Obtainium> {
|
||||
? lightColorScheme
|
||||
: darkColorScheme,
|
||||
fontFamily: 'Metropolis'),
|
||||
home: const HomePage());
|
||||
home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
}, child: const HomePage()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>)) {
|
||||
|
@ -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';
|
||||
@ -41,6 +42,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
|
||||
bool doingSomething = gettingAppInfo || searching;
|
||||
|
||||
@ -85,8 +89,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getTrackOnlyConfirmationIfNeeded(
|
||||
bool userPickedTrackOnly, SettingsProvider settingsProvider,
|
||||
Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
|
||||
{bool ignoreHideSetting = false}) async {
|
||||
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
|
||||
if (useTrackOnly &&
|
||||
@ -138,16 +141,14 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
gettingAppInfo = true;
|
||||
});
|
||||
try {
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||
App? app;
|
||||
if ((await getTrackOnlyConfirmationIfNeeded(
|
||||
userPickedTrackOnly, settingsProvider)) &&
|
||||
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
||||
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
||||
userPickedTrackOnly))) {
|
||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||
app = await sourceProvider.getApp(
|
||||
pickedSource!, userInput, additionalSettings,
|
||||
pickedSource!, userInput.trim(), additionalSettings,
|
||||
trackOnlyOverride: trackOnly,
|
||||
overrideSource: pickedSourceOverride,
|
||||
inferAppIdIfOptional: inferAppIdIfOptional);
|
||||
@ -163,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) {
|
||||
@ -410,7 +412,13 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
),
|
||||
GeneratedForm(
|
||||
key: Key(pickedSource.runtimeType.toString()),
|
||||
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
||||
items: [
|
||||
...pickedSource!.combinedAppSpecificSettingFormItems,
|
||||
...(pickedSourceOverride != null
|
||||
? pickedSource!.sourceConfigSettingFormItems
|
||||
.map((e) => [e])
|
||||
: [])
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
@ -455,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(
|
||||
@ -504,15 +510,29 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
HTML().runtimeType.toString()))
|
||||
getHTMLSourceOverrideDropdown(),
|
||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (pickedSource != null)
|
||||
getAdditionalOptsCol()
|
||||
else
|
||||
getSourcesListWidget(),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return val.data != null && val.data!.isNotEmpty
|
||||
? Text(
|
||||
val.data!,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
future: pickedSource?.getSourceNote()),
|
||||
SizedBox(
|
||||
height: pickedSource != null ? 16 : 96,
|
||||
),
|
||||
if (pickedSource != null) getAdditionalOptsCol(),
|
||||
if (pickedSource == null)
|
||||
const Divider(
|
||||
height: 48,
|
||||
),
|
||||
if (pickedSource == null) getSourcesListWidget(),
|
||||
SizedBox(
|
||||
height: pickedSource != null ? 8 : 2,
|
||||
),
|
||||
])),
|
||||
)
|
||||
|
@ -152,11 +152,11 @@ class _AppPageState extends State<AppPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 125),
|
||||
app?.installedInfo != null
|
||||
const SizedBox(height: 20),
|
||||
app?.icon != null
|
||||
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Image.memory(
|
||||
app!.installedInfo!.icon!,
|
||||
app!.icon!,
|
||||
height: 150,
|
||||
gaplessPlayback: true,
|
||||
)
|
||||
@ -339,11 +339,17 @@ class _AppPageState extends State<AppPage> {
|
||||
HapticFeedback.heavyImpact();
|
||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||
app?.app.id != null ? [app!.app.id] : [],
|
||||
globalNavigatorKey.currentContext);
|
||||
globalNavigatorKey.currentContext,
|
||||
);
|
||||
if (app?.app.installedVersion != null && !trackOnly) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(tr('appsUpdated'), context);
|
||||
}
|
||||
if (res.isNotEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(e, context);
|
||||
}
|
||||
}
|
||||
@ -431,8 +437,7 @@ class _AppPageState extends State<AppPage> {
|
||||
? getResetInstallStatusButton()
|
||||
: getInstallOrUpdateButton()),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
@ -445,13 +450,9 @@ class _AppPageState extends State<AppPage> {
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).colorScheme.error),
|
||||
child: Text(tr('remove')),
|
||||
)),
|
||||
tooltip: tr('remove'),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
])),
|
||||
if (app?.downloadProgress != null)
|
||||
Padding(
|
||||
@ -463,8 +464,17 @@ class _AppPageState extends State<AppPage> {
|
||||
],
|
||||
));
|
||||
|
||||
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
|
||||
|
@ -393,9 +393,9 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
|
||||
getAppIcon(int appIndex) {
|
||||
return listedApps[appIndex].installedInfo != null
|
||||
return listedApps[appIndex].icon != null
|
||||
? Image.memory(
|
||||
listedApps[appIndex].installedInfo!.icon!,
|
||||
listedApps[appIndex].icon!,
|
||||
gaplessPlayback: true,
|
||||
)
|
||||
: Row(
|
||||
@ -448,6 +448,19 @@ class AppsPageState extends State<AppsPage> {
|
||||
: const SizedBox.shrink(),
|
||||
GestureDetector(
|
||||
onTap: showChangesFn,
|
||||
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,
|
||||
@ -455,7 +468,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width / 4),
|
||||
maxWidth:
|
||||
MediaQuery.of(context).size.width / 4),
|
||||
child: Text(getVersionText(index),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.end)),
|
||||
@ -464,7 +478,8 @@ class AppsPageState extends State<AppsPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
getChangesButtonString(index, showChangesFn != null),
|
||||
getChangesButtonString(
|
||||
index, showChangesFn != null),
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
decoration: showChangesFn != null
|
||||
@ -474,7 +489,7 @@ class AppsPageState extends State<AppsPage> {
|
||||
],
|
||||
),
|
||||
],
|
||||
))
|
||||
)))
|
||||
],
|
||||
);
|
||||
|
||||
@ -543,7 +558,6 @@ class AppsPageState extends State<AppsPage> {
|
||||
: FontWeight.normal)),
|
||||
trailing: listedApps[index].downloadProgress != null
|
||||
? SizedBox(
|
||||
width: 90,
|
||||
child: Text(
|
||||
listedApps[index].downloadProgress! >= 0
|
||||
? tr('percentProgress', args: [
|
||||
@ -644,15 +658,15 @@ class AppsPageState extends State<AppsPage> {
|
||||
label: tr('installX', args: [
|
||||
plural('apps', newInstallIdsAllOrSelected.length)
|
||||
]),
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty));
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isEmpty));
|
||||
}
|
||||
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
|
||||
formItems.add(GeneratedFormSwitch('trackonlies',
|
||||
label: tr('markXTrackOnlyAsUpdated', args: [
|
||||
plural('apps', trackOnlyUpdateIdsAllOrSelected.length)
|
||||
]),
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty ||
|
||||
newInstallIdsAllOrSelected.isNotEmpty));
|
||||
defaultValue: existingUpdateIdsAllOrSelected.isEmpty &&
|
||||
newInstallIdsAllOrSelected.isEmpty));
|
||||
}
|
||||
showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
@ -685,11 +699,14 @@ class AppsPageState extends State<AppsPage> {
|
||||
}
|
||||
appsProvider
|
||||
.downloadAndInstallLatestApps(
|
||||
toInstall, globalNavigatorKey.currentContext,
|
||||
settingsProvider: settingsProvider)
|
||||
toInstall, globalNavigatorKey.currentContext)
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
return <String>[];
|
||||
}).then((value) {
|
||||
if (shouldInstallUpdates) {
|
||||
showError(tr('appsUpdated'), context);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
@ -300,6 +306,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: settingsProvider.getExportDir(),
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@ -310,7 +321,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
importInProgress
|
||||
? null
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport')))),
|
||||
child: Text(tr(snapshot.data != null
|
||||
? 'obtainiumExport'
|
||||
: 'pickExportDir')),
|
||||
)),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
@ -323,6 +337,39 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
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(
|
||||
children: [
|
||||
@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
fontStyle: FontStyle.italic, fontSize: 12)),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
)
|
||||
),
|
||||
],
|
||||
)))
|
||||
]));
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:obtainium/components/custom_app_bar.dart';
|
||||
@ -166,9 +168,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
});
|
||||
|
||||
var sourceSpecificFields = sourceProvider.sources.map((e) {
|
||||
if (e.additionalSourceSpecificSettingFormItems.isNotEmpty) {
|
||||
if (e.sourceConfigSettingFormItems.isNotEmpty) {
|
||||
return GeneratedForm(
|
||||
items: e.additionalSourceSpecificSettingFormItems.map((e) {
|
||||
items: e.sourceConfigSettingFormItems.map((e) {
|
||||
e.defaultValue = settingsProvider.getSettingString(e.key);
|
||||
return [e];
|
||||
}).toList(),
|
||||
@ -184,6 +186,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
});
|
||||
|
||||
const height8 = SizedBox(
|
||||
height: 8,
|
||||
);
|
||||
|
||||
const height16 = SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
@ -211,6 +217,72 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
intervalDropdown,
|
||||
FutureBuilder(
|
||||
builder: (ctx, val) {
|
||||
return (val.data?.version.sdkInt ?? 0) >= 30
|
||||
? Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr(
|
||||
'enableBackgroundUpdates'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.enableBackgroundUpdates,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.enableBackgroundUpdates =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height8,
|
||||
Text(tr('backgroundUpdateReqsExplanation'),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall),
|
||||
Text(tr('backgroundUpdateLimitsExplanation'),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall),
|
||||
height8,
|
||||
if (settingsProvider
|
||||
.enableBackgroundUpdates)
|
||||
Column(
|
||||
children: [
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr(
|
||||
'bgUpdatesOnWiFiOnly'))),
|
||||
Switch(
|
||||
value: settingsProvider
|
||||
.bgUpdatesOnWiFiOnly,
|
||||
onChanged: (value) {
|
||||
settingsProvider
|
||||
.bgUpdatesOnWiFiOnly =
|
||||
value;
|
||||
})
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
future: DeviceInfoPlugin().androidInfo),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -403,8 +475,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.reversePageTransitions,
|
||||
onChanged: settingsProvider
|
||||
.disablePageTransitions
|
||||
? null
|
||||
: (value) {
|
||||
settingsProvider
|
||||
.reversePageTransitions = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
height16,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(tr('highlightTouchTargets'))),
|
||||
Switch(
|
||||
value:
|
||||
settingsProvider.highlightTouchTargets,
|
||||
onChanged: (value) {
|
||||
settingsProvider.reversePageTransitions =
|
||||
settingsProvider.highlightTouchTargets =
|
||||
value;
|
||||
})
|
||||
],
|
||||
@ -459,7 +549,44 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
label: Text(tr('appLogs'))),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 32,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Flexible(child: Text('Debug Menu')),
|
||||
Switch(
|
||||
value: settingsProvider.showDebugOpts,
|
||||
onChanged: (value) {
|
||||
settingsProvider.showDebugOpts = value;
|
||||
})
|
||||
],
|
||||
),
|
||||
if (settingsProvider.showDebugOpts)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
height16,
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
AndroidAlarmManager.oneShot(
|
||||
const Duration(seconds: 0),
|
||||
bgUpdateCheckAlarmId + 200,
|
||||
bgUpdateCheck);
|
||||
showError(
|
||||
'Background task started - check logs.',
|
||||
context);
|
||||
},
|
||||
child:
|
||||
const Text('Run Background Update Check Now'))
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -4,22 +4,25 @@
|
||||
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';
|
||||
import 'package:android_package_installer/android_package_installer.dart';
|
||||
import 'package:android_package_manager/android_package_manager.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:installed_apps/app_info.dart';
|
||||
import 'package:installed_apps/installed_apps.dart';
|
||||
import 'package:obtainium/components/generated_form.dart';
|
||||
import 'package:obtainium/components/generated_form_modal.dart';
|
||||
import 'package:obtainium/custom_errors.dart';
|
||||
import 'package:obtainium/main.dart';
|
||||
import 'package:obtainium/providers/logs_provider.dart';
|
||||
import 'package:obtainium/providers/notifications_provider.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
import 'package:package_archive_info/package_archive_info.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -28,17 +31,21 @@ 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();
|
||||
|
||||
class AppInMemory {
|
||||
late App app;
|
||||
double? downloadProgress;
|
||||
AppInfo? installedInfo;
|
||||
PackageInfo? installedInfo;
|
||||
Uint8List? icon;
|
||||
|
||||
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
|
||||
AppInMemory(this.app, this.downloadProgress, this.installedInfo, this.icon);
|
||||
AppInMemory deepCopy() =>
|
||||
AppInMemory(app.deepCopy(), downloadProgress, installedInfo);
|
||||
AppInMemory(app.deepCopy(), downloadProgress, installedInfo, icon);
|
||||
|
||||
String get name => app.overrideName ?? installedInfo?.name ?? app.finalName;
|
||||
String get name => app.overrideName ?? app.finalName;
|
||||
}
|
||||
|
||||
class DownloadedApk {
|
||||
@ -97,6 +104,41 @@ Set<String> findStandardFormatsForVersion(String version, bool strict) {
|
||||
return results;
|
||||
}
|
||||
|
||||
moveStrToEnd(List<String> arr, String str, {String? strB}) {
|
||||
String? temp;
|
||||
arr.removeWhere((element) {
|
||||
bool res = element == str || element == strB;
|
||||
if (res) {
|
||||
temp = element;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
if (temp != null) {
|
||||
arr = [...arr, temp!];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
List<MapEntry<String, int>> moveStrToEndMapEntryWithCount(
|
||||
List<MapEntry<String, int>> arr, MapEntry<String, int> str,
|
||||
{MapEntry<String, int>? strB}) {
|
||||
MapEntry<String, int>? temp;
|
||||
arr.removeWhere((element) {
|
||||
bool resA = element.key == str.key;
|
||||
bool resB = element.key == strB?.key;
|
||||
if (resA) {
|
||||
temp = str;
|
||||
} else if (resB) {
|
||||
temp = strB;
|
||||
}
|
||||
return resA || resB;
|
||||
});
|
||||
if (temp != null) {
|
||||
arr = [...arr, temp!];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
class AppsProvider with ChangeNotifier {
|
||||
// In memory App state (should always be kept in sync with local storage versions)
|
||||
Map<String, AppInMemory> apps = {};
|
||||
@ -109,10 +151,11 @@ 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());
|
||||
|
||||
AppsProvider() {
|
||||
AppsProvider({isBg = false}) {
|
||||
// Subscribe to changes in the app foreground status
|
||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||
@ -120,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;
|
||||
@ -130,17 +174,21 @@ class AppsProvider with ChangeNotifier {
|
||||
APKDir.createSync();
|
||||
}
|
||||
}
|
||||
// Load Apps into memory (in background, this is done later instead of in the constructor)
|
||||
if (!isBg) {
|
||||
// Load Apps into memory (in background processes, this is done later instead of in the constructor)
|
||||
await loadApps();
|
||||
// Delete any partial APKs
|
||||
// Delete any partial APKs (if safe to do so)
|
||||
var cutoff = DateTime.now().subtract(const Duration(days: 7));
|
||||
APKDir.listSync()
|
||||
.where((element) =>
|
||||
element.path.endsWith('.part') ||
|
||||
element.statSync().modified.isBefore(cutoff))
|
||||
.forEach((partialApk) {
|
||||
if (!areDownloadsRunning()) {
|
||||
partialApk.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
@ -154,6 +202,7 @@ class AppsProvider with ChangeNotifier {
|
||||
useExisting: useExisting, headers: headers);
|
||||
} catch (e) {
|
||||
if (retries > 0 && e is ClientException) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
return await downloadFileWithRetry(url, fileNameNoExt, onProgress,
|
||||
useExisting: useExisting, headers: headers, retries: (retries - 1));
|
||||
} else {
|
||||
@ -170,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';
|
||||
@ -214,19 +263,19 @@ class AppsProvider with ChangeNotifier {
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo,
|
||||
Future<File> handleAPKIDChange(App app, PackageInfo newInfo,
|
||||
File downloadedFile, String downloadUrl) async {
|
||||
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
|
||||
// The former case should be handled (give the App its real ID), the latter is a security issue
|
||||
if (app.id != newInfo.packageName) {
|
||||
var isTempId = SourceProvider().isTempId(app);
|
||||
if (apps[app.id] != null && !isTempId && !app.allowIdChange) {
|
||||
throw IDChangedError(newInfo.packageName);
|
||||
throw IDChangedError(newInfo.packageName!);
|
||||
}
|
||||
var idChangeWasAllowed = app.allowIdChange;
|
||||
app.allowIdChange = false;
|
||||
var originalAppId = app.id;
|
||||
app.id = newInfo.packageName;
|
||||
app.id = newInfo.packageName!;
|
||||
downloadedFile = downloadedFile.renameSync(
|
||||
'${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}');
|
||||
if (apps[originalAppId] != null) {
|
||||
@ -237,9 +286,8 @@ class AppsProvider with ChangeNotifier {
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
Future<Object> downloadApp(App app, BuildContext? context) async {
|
||||
NotificationsProvider? notificationsProvider =
|
||||
context?.read<NotificationsProvider>();
|
||||
Future<Object> downloadApp(App app, BuildContext? context,
|
||||
{NotificationsProvider? notificationsProvider}) async {
|
||||
var notifId = DownloadNotification(app.finalName, 0).id;
|
||||
if (apps[app.id] != null) {
|
||||
apps[app.id]!.downloadProgress = 0;
|
||||
@ -254,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;
|
||||
@ -275,11 +325,12 @@ class AppsProvider with ChangeNotifier {
|
||||
notif = DownloadNotification(app.finalName, -1);
|
||||
notificationsProvider?.notify(notif);
|
||||
}
|
||||
PackageArchiveInfo? newInfo;
|
||||
PackageInfo? newInfo;
|
||||
var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk');
|
||||
Directory? xapkDir;
|
||||
if (isAPK) {
|
||||
newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path);
|
||||
newInfo = await pm.getPackageArchiveInfo(
|
||||
archiveFilePath: downloadedFile.path);
|
||||
} else {
|
||||
// Assume XAPK
|
||||
String xapkDirPath = '${downloadedFile.path}-dir';
|
||||
@ -289,10 +340,11 @@ class AppsProvider with ChangeNotifier {
|
||||
.listSync()
|
||||
.where((e) => e.path.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
newInfo = await PackageArchiveInfo.fromPath(apks.first.path);
|
||||
newInfo =
|
||||
await pm.getPackageArchiveInfo(archiveFilePath: apks.first.path);
|
||||
}
|
||||
downloadedFile =
|
||||
await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl);
|
||||
await handleAPKIDChange(app, newInfo!, downloadedFile, downloadUrl);
|
||||
// Delete older versions of the file if any
|
||||
for (var file in downloadedFile.parent.listSync()) {
|
||||
var fn = file.path.split('/').last;
|
||||
@ -321,16 +373,42 @@ class AppsProvider with ChangeNotifier {
|
||||
.isNotEmpty;
|
||||
|
||||
Future<bool> canInstallSilently(App app) async {
|
||||
if (app.id == obtainiumId) {
|
||||
return false;
|
||||
// TODO: Uncomment the below if silent updates are ever figured out
|
||||
// // NOTE: This is unreliable - try to get from OS in the future
|
||||
// if (app.apkUrls.length > 1) {
|
||||
// return false;
|
||||
// }
|
||||
// var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||
// return app.installedVersion != null &&
|
||||
// osInfo.version.sdkInt >= 30 &&
|
||||
// osInfo.version.release.compareTo('12') >= 0;
|
||||
}
|
||||
if (!settingsProvider.enableBackgroundUpdates) {
|
||||
return false;
|
||||
}
|
||||
if (app.additionalSettings['exemptFromBackgroundUpdates'] == true) {
|
||||
return false;
|
||||
}
|
||||
if (app.apkUrls.length > 1) {
|
||||
// Manual API selection means silent install is not possible
|
||||
return false;
|
||||
}
|
||||
|
||||
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||
String? installerPackageName;
|
||||
try {
|
||||
installerPackageName = osInfo.version.sdkInt >= 30
|
||||
? (await pm.getInstallSourceInfo(packageName: app.id))
|
||||
?.installingPackageName
|
||||
: (await pm.getInstallerPackageName(packageName: app.id));
|
||||
} catch (e) {
|
||||
// Probably not installed - ignore
|
||||
}
|
||||
if (installerPackageName != obtainiumId) {
|
||||
// If we did not install the app (or it isn't installed), silent install is not possible
|
||||
return false;
|
||||
}
|
||||
int? targetSDK =
|
||||
(await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion;
|
||||
|
||||
// The OS must also be new enough and the APK should target a new enough API
|
||||
return osInfo.version.sdkInt >= 31 &&
|
||||
targetSDK != null &&
|
||||
targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
|
||||
(osInfo.version.sdkInt - 3);
|
||||
}
|
||||
|
||||
Future<void> waitForUserToReturnToForeground(BuildContext context) async {
|
||||
@ -344,14 +422,8 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> canDowngradeApps() async {
|
||||
try {
|
||||
await InstalledApps.getAppInfo('com.berdik.letmedowngrade');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Future<bool> canDowngradeApps() async =>
|
||||
(await getInstalledInfo('com.berdik.letmedowngrade')) != null;
|
||||
|
||||
Future<void> unzipFile(String filePath, String destinationPath) async {
|
||||
await ZipFile.extractToDirectory(
|
||||
@ -359,13 +431,13 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> installXApkDir(DownloadedXApkDir dir,
|
||||
{bool silent = false}) async {
|
||||
{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
|
||||
try {
|
||||
var somethingInstalled = false;
|
||||
Object? firstError;
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
for (var file in dir.extracted
|
||||
.listSync(recursive: true, followLinks: false)
|
||||
.whereType<File>()) {
|
||||
@ -373,11 +445,11 @@ class AppsProvider with ChangeNotifier {
|
||||
try {
|
||||
somethingInstalled = somethingInstalled ||
|
||||
await installApk(DownloadedApk(dir.appId, file),
|
||||
silent: silent);
|
||||
needsBGWorkaround: needsBGWorkaround);
|
||||
} catch (e) {
|
||||
logs.add(
|
||||
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
|
||||
firstError ??= e;
|
||||
errors.add(dir.appId, e.toString());
|
||||
}
|
||||
} else if (file.path.toLowerCase().endsWith('.obb')) {
|
||||
await moveObbFile(file, dir.appId);
|
||||
@ -385,28 +457,35 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (somethingInstalled) {
|
||||
dir.file.delete(recursive: true);
|
||||
} else if (firstError != null) {
|
||||
throw firstError;
|
||||
} else if (errors.content.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
} finally {
|
||||
dir.extracted.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> installApk(DownloadedApk file, {bool silent = false}) async {
|
||||
// TODO: Use 'silent' when/if ever possible
|
||||
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||
AppInfo? appInfo;
|
||||
try {
|
||||
appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id);
|
||||
} catch (e) {
|
||||
// OK
|
||||
}
|
||||
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);
|
||||
if (appInfo != null &&
|
||||
int.parse(newInfo.buildNumber) < appInfo.versionCode! &&
|
||||
newInfo!.versionCode! < appInfo.versionCode! &&
|
||||
!(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;
|
||||
@ -476,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(
|
||||
@ -499,7 +577,9 @@ class AppsProvider with ChangeNotifier {
|
||||
// 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}) async {
|
||||
{NotificationsProvider? notificationsProvider}) async {
|
||||
notificationsProvider =
|
||||
notificationsProvider ?? context?.read<NotificationsProvider>();
|
||||
List<String> appsToInstall = [];
|
||||
List<String> trackOnlyAppsToUpdate = [];
|
||||
// For all specified Apps, filter out those for which:
|
||||
@ -545,22 +625,15 @@ class AppsProvider with ChangeNotifier {
|
||||
List<String> installedIds = [];
|
||||
|
||||
// Move Obtainium to the end of the line (let all other apps update first)
|
||||
String? temp;
|
||||
appsToInstall.removeWhere((element) {
|
||||
bool res = element == obtainiumId || element == obtainiumTempId;
|
||||
if (res) {
|
||||
temp = element;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
if (temp != null) {
|
||||
appsToInstall = [...appsToInstall, temp!];
|
||||
}
|
||||
appsToInstall =
|
||||
moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
|
||||
|
||||
for (var id in appsToInstall) {
|
||||
try {
|
||||
var downloadedArtifact =
|
||||
// ignore: use_build_context_synchronously
|
||||
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
|
||||
await downloadApp(apps[id]!.app, context,
|
||||
notificationsProvider: notificationsProvider);
|
||||
DownloadedApk? downloadedFile;
|
||||
DownloadedXApkDir? downloadedDir;
|
||||
if (downloadedArtifact is DownloadedApk) {
|
||||
@ -568,11 +641,9 @@ class AppsProvider with ChangeNotifier {
|
||||
} else {
|
||||
downloadedDir = downloadedArtifact as DownloadedXApkDir;
|
||||
}
|
||||
bool willBeSilent = await canInstallSilently(
|
||||
apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app);
|
||||
willBeSilent = false; // TODO: Remove this when silent updates work
|
||||
if (!(await settingsProvider?.getInstallPermission(enforce: false) ??
|
||||
true)) {
|
||||
var appId = downloadedFile?.appId ?? downloadedDir!.appId;
|
||||
bool willBeSilent = await canInstallSilently(apps[appId]!.app);
|
||||
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
if (!willBeSilent && context != null) {
|
||||
@ -583,9 +654,22 @@ class AppsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
try {
|
||||
if (downloadedFile != null) {
|
||||
await installApk(downloadedFile, silent: willBeSilent);
|
||||
if (willBeSilent && context == null) {
|
||||
installApk(downloadedFile, needsBGWorkaround: true);
|
||||
} else {
|
||||
await installXApkDir(downloadedDir!, silent: willBeSilent);
|
||||
await installApk(downloadedFile);
|
||||
}
|
||||
} else {
|
||||
if (willBeSilent && context == null) {
|
||||
installXApkDir(downloadedDir!, needsBGWorkaround: true);
|
||||
} else {
|
||||
await installXApkDir(downloadedDir!);
|
||||
}
|
||||
}
|
||||
if (willBeSilent && context == null) {
|
||||
notificationsProvider?.notify(SilentUpdateAttemptNotification(
|
||||
[apps[appId]!.app],
|
||||
id: appId.hashCode));
|
||||
}
|
||||
} finally {
|
||||
apps[id]?.downloadProgress = null;
|
||||
@ -601,41 +685,29 @@ class AppsProvider with ChangeNotifier {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
NotificationsProvider().cancel(UpdateNotification([]).id);
|
||||
|
||||
return installedIds;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
return appsDir;
|
||||
}
|
||||
|
||||
Future<AppInfo?> getInstalledInfo(String? packageName) async {
|
||||
Future<PackageInfo?> getInstalledInfo(String? packageName) async {
|
||||
if (packageName != null) {
|
||||
try {
|
||||
return await InstalledApps.getAppInfo(packageName);
|
||||
return await pm.getPackageInfo(packageName: packageName);
|
||||
} catch (e) {
|
||||
// OK
|
||||
print(e); // OK
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> doesInstalledAppsPluginWork() async {
|
||||
bool res = false;
|
||||
try {
|
||||
res = (await InstalledApps.getAppInfo(obtainiumId)).versionName != null;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool isVersionDetectionPossible(AppInMemory? app) {
|
||||
return app?.app.additionalSettings['trackOnly'] != true &&
|
||||
app?.app.additionalSettings['versionDetection'] !=
|
||||
@ -649,7 +721,8 @@ class AppsProvider with ChangeNotifier {
|
||||
|
||||
// Given an App and it's on-device info...
|
||||
// Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version
|
||||
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
||||
App? getCorrectedInstallStatusAppIfPossible(
|
||||
App app, PackageInfo? installedInfo) {
|
||||
var modded = false;
|
||||
var trackOnly = app.additionalSettings['trackOnly'] == true;
|
||||
var noVersionDetection = app.additionalSettings['versionDetection'] !=
|
||||
@ -695,22 +768,12 @@ class AppsProvider with ChangeNotifier {
|
||||
if (installedInfo != null &&
|
||||
app.additionalSettings['versionDetection'] ==
|
||||
'standardVersionDetection' &&
|
||||
!isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) {
|
||||
!isVersionDetectionPossible(
|
||||
AppInMemory(app, null, installedInfo, null))) {
|
||||
app.additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||
logs.add('Could not reconcile version formats for: ${app.id}');
|
||||
modded = true;
|
||||
}
|
||||
// if (app.installedVersion != null &&
|
||||
// app.additionalSettings['versionDetection'] ==
|
||||
// 'standardVersionDetection') {
|
||||
// var correctedInstalledVersion =
|
||||
// reconcileVersionDifferences(app.installedVersion!, app.latestVersion);
|
||||
// if (correctedInstalledVersion == null) {
|
||||
// app.additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||
// logs.add('Could not reconcile version formats for: ${app.id}');
|
||||
// modded = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return modded ? app : null;
|
||||
}
|
||||
@ -749,7 +812,7 @@ class AppsProvider with ChangeNotifier {
|
||||
: false;
|
||||
}
|
||||
|
||||
Future<void> loadApps() async {
|
||||
Future<void> loadApps({String? singleId}) async {
|
||||
while (loadingApps) {
|
||||
await Future.delayed(const Duration(microseconds: 1));
|
||||
}
|
||||
@ -760,6 +823,10 @@ class AppsProvider with ChangeNotifier {
|
||||
List<App?> newApps = (await getAppsDir()) // Parse Apps from JSON
|
||||
.listSync()
|
||||
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
||||
.where((item) =>
|
||||
singleId == null ||
|
||||
item.path.split('/').last.toLowerCase() ==
|
||||
'${singleId.toLowerCase()}.json')
|
||||
.map((e) {
|
||||
try {
|
||||
return App.fromJson(jsonDecode(File(e.path).readAsStringSync()));
|
||||
@ -779,9 +846,9 @@ class AppsProvider with ChangeNotifier {
|
||||
sp.getSource(app.url, overrideSource: app.overrideSource);
|
||||
apps.update(
|
||||
app.id,
|
||||
(value) =>
|
||||
AppInMemory(app, value.downloadProgress, value.installedInfo),
|
||||
ifAbsent: () => AppInMemory(app, null, null));
|
||||
(value) => AppInMemory(
|
||||
app, value.downloadProgress, value.installedInfo, value.icon),
|
||||
ifAbsent: () => AppInMemory(app, null, null, null));
|
||||
} catch (e) {
|
||||
errors.add([app.id, app.finalName, e.toString()]);
|
||||
}
|
||||
@ -794,10 +861,16 @@ class AppsProvider with ChangeNotifier {
|
||||
AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
|
||||
}
|
||||
|
||||
if (await doesInstalledAppsPluginWork()) {
|
||||
for (var app in apps.values) {
|
||||
// Check install status for each App (slow)
|
||||
// Get install status and other OS info for each App (slow)
|
||||
apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id);
|
||||
apps[app.app.id]?.icon =
|
||||
await apps[app.app.id]?.installedInfo?.applicationInfo?.getAppIcon();
|
||||
apps[app.app.id]?.app.name = await (apps[app.app.id]
|
||||
?.installedInfo
|
||||
?.applicationInfo
|
||||
?.getAppLabel()) ??
|
||||
app.name;
|
||||
notifyListeners();
|
||||
}
|
||||
// Reconcile version differences
|
||||
@ -817,14 +890,11 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadingApps = false;
|
||||
notifyListeners();
|
||||
@ -833,12 +903,12 @@ class AppsProvider with ChangeNotifier {
|
||||
Future<void> saveApps(List<App> apps,
|
||||
{bool attemptToCorrectInstallStatus = true,
|
||||
bool onlyIfExists = true}) async {
|
||||
attemptToCorrectInstallStatus =
|
||||
attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork());
|
||||
attemptToCorrectInstallStatus = attemptToCorrectInstallStatus;
|
||||
for (var a in apps) {
|
||||
var app = a.deepCopy();
|
||||
AppInfo? info = await getInstalledInfo(app.id);
|
||||
app.name = info?.name ?? app.name;
|
||||
PackageInfo? info = await getInstalledInfo(app.id);
|
||||
var icon = await info?.applicationInfo?.getAppIcon();
|
||||
app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name;
|
||||
if (attemptToCorrectInstallStatus) {
|
||||
app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app;
|
||||
}
|
||||
@ -847,9 +917,10 @@ class AppsProvider with ChangeNotifier {
|
||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||
}
|
||||
try {
|
||||
this.apps.update(
|
||||
app.id, (value) => AppInMemory(app, value.downloadProgress, info),
|
||||
ifAbsent: onlyIfExists ? null : () => AppInMemory(app, null, info));
|
||||
this.apps.update(app.id,
|
||||
(value) => AppInMemory(app, value.downloadProgress, info, icon),
|
||||
ifAbsent:
|
||||
onlyIfExists ? null : () => AppInMemory(app, null, info, icon));
|
||||
} catch (e) {
|
||||
if (e is! ArgumentError || e.name != 'key') {
|
||||
rethrow;
|
||||
@ -857,6 +928,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
exportApps(isAuto: true);
|
||||
}
|
||||
|
||||
Future<void> removeApps(List<String> appIds) async {
|
||||
@ -878,6 +950,7 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
if (appIds.isNotEmpty) {
|
||||
notifyListeners();
|
||||
exportApps(isAuto: true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -891,6 +964,7 @@ class AppsProvider with ChangeNotifier {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
primaryActionColour: Theme.of(context).colorScheme.error,
|
||||
title: plural('removeAppQuestion', apps.length),
|
||||
items: !showUninstallOption
|
||||
? []
|
||||
@ -963,6 +1037,22 @@ class AppsProvider with ChangeNotifier {
|
||||
return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
|
||||
}
|
||||
|
||||
List<String> getAppsSortedByUpdateCheckTime(
|
||||
{DateTime? ignoreAppsCheckedAfter}) {
|
||||
List<String> appIds = apps.values
|
||||
.where((app) =>
|
||||
app.app.lastUpdateCheck == null ||
|
||||
ignoreAppsCheckedAfter == null ||
|
||||
app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter))
|
||||
.map((e) => e.app.id)
|
||||
.toList();
|
||||
appIds.sort((a, b) =>
|
||||
(apps[a]!.app.lastUpdateCheck ?? DateTime.fromMicrosecondsSinceEpoch(0))
|
||||
.compareTo(apps[b]!.app.lastUpdateCheck ??
|
||||
DateTime.fromMicrosecondsSinceEpoch(0)));
|
||||
return appIds;
|
||||
}
|
||||
|
||||
Future<List<App>> checkUpdates(
|
||||
{DateTime? ignoreAppsCheckedAfter,
|
||||
bool throwErrorsForRetry = false}) async {
|
||||
@ -971,17 +1061,8 @@ class AppsProvider with ChangeNotifier {
|
||||
if (!gettingUpdates) {
|
||||
gettingUpdates = true;
|
||||
try {
|
||||
List<String> appIds = apps.values
|
||||
.where((app) =>
|
||||
app.app.lastUpdateCheck == null ||
|
||||
ignoreAppsCheckedAfter == null ||
|
||||
app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter))
|
||||
.map((e) => e.app.id)
|
||||
.toList();
|
||||
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
|
||||
DateTime.fromMicrosecondsSinceEpoch(0))
|
||||
.compareTo(apps[b]!.app.lastUpdateCheck ??
|
||||
DateTime.fromMicrosecondsSinceEpoch(0)));
|
||||
List<String> appIds = getAppsSortedByUpdateCheckTime(
|
||||
ignoreAppsCheckedAfter: ignoreAppsCheckedAfter);
|
||||
for (int i = 0; i < appIds.length; i++) {
|
||||
App? newApp;
|
||||
try {
|
||||
@ -1026,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 {
|
||||
@ -1198,3 +1299,217 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Background updater function
|
||||
///
|
||||
/// @param List<String>? toCheck: The appIds to check for updates (default to all apps sorted by last update check time)
|
||||
///
|
||||
/// @param List<String>? toInstall: The appIds to attempt to update (defaults to an empty array)
|
||||
///
|
||||
/// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0)
|
||||
///
|
||||
/// When toCheck is empty, the function is in "install mode" (else it is in "update mode").
|
||||
/// In update mode, all apps in toCheck are checked for updates.
|
||||
/// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified.
|
||||
/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries.
|
||||
///
|
||||
/// Once all update checks are complete, the function is called again in install mode.
|
||||
/// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown).
|
||||
/// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries.
|
||||
///
|
||||
/// In either mode, if the function fails after the maximum number of tries, the user is notified.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await AndroidAlarmManager.initialize();
|
||||
await loadTranslations();
|
||||
|
||||
LogsProvider logs = LogsProvider();
|
||||
NotificationsProvider notificationsProvider = NotificationsProvider();
|
||||
AppsProvider appsProvider = AppsProvider(isBg: true);
|
||||
await appsProvider.loadApps();
|
||||
|
||||
int maxAttempts = 4;
|
||||
|
||||
params ??= {};
|
||||
if (params['toCheck'] == null) {
|
||||
appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
|
||||
}
|
||||
List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
|
||||
...(params['toCheck']
|
||||
?.map((entry) => MapEntry<String, int>(
|
||||
entry['key'] as String, entry['value'] as int))
|
||||
.toList() ??
|
||||
appsProvider
|
||||
.getAppsSortedByUpdateCheckTime()
|
||||
.map((e) => MapEntry(e, 0)))
|
||||
];
|
||||
List<MapEntry<String, int>> toInstall = <MapEntry<String, int>>[
|
||||
...(params['toInstall']
|
||||
?.map((entry) => MapEntry<String, int>(
|
||||
entry['key'] as String, entry['value'] as int))
|
||||
.toList() ??
|
||||
(<List<MapEntry<String, int>>>[]))
|
||||
];
|
||||
|
||||
bool installMode = toCheck.isEmpty &&
|
||||
toInstall.isNotEmpty; // Task is either in update mode or install mode
|
||||
|
||||
logs.add(
|
||||
'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).');
|
||||
|
||||
if (!installMode) {
|
||||
// If in update mode...
|
||||
var didCompleteChecking = false;
|
||||
CheckingUpdatesNotification? notif;
|
||||
var networkRestricted = false;
|
||||
if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
|
||||
var netResult = await (Connectivity().checkConnectivity());
|
||||
networkRestricted = (netResult != ConnectivityResult.wifi) &&
|
||||
(netResult != ConnectivityResult.ethernet);
|
||||
}
|
||||
// Loop through all updates and check each
|
||||
List<App> toNotify = [];
|
||||
try {
|
||||
for (int i = 0; i < toCheck.length; i++) {
|
||||
var appId = toCheck[i].key;
|
||||
var attemptCount = toCheck[i].value + 1;
|
||||
AppInMemory? app = appsProvider.apps[appId];
|
||||
if (app?.app.installedVersion != null) {
|
||||
try {
|
||||
notificationsProvider.notify(
|
||||
notif = CheckingUpdatesNotification(app?.name ?? appId),
|
||||
cancelExisting: true);
|
||||
App? newApp = await appsProvider.checkUpdate(appId);
|
||||
if (newApp != null) {
|
||||
if (networkRestricted ||
|
||||
!(await appsProvider.canInstallSilently(app!.app))) {
|
||||
toNotify.add(newApp);
|
||||
} else {
|
||||
toInstall.add(MapEntry(appId, 0));
|
||||
}
|
||||
}
|
||||
if (i == (toCheck.length - 1)) {
|
||||
didCompleteChecking = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue checking shortly
|
||||
logs.add(
|
||||
'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.');
|
||||
if (attemptCount < maxAttempts) {
|
||||
var remainingSeconds = e is RateLimitError
|
||||
? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60))
|
||||
: e is ClientException
|
||||
? (15 * 60)
|
||||
: pow(attemptCount, 2).toInt();
|
||||
logs.add(
|
||||
'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
|
||||
var remainingToCheck = moveStrToEndMapEntryWithCount(
|
||||
toCheck.sublist(i), MapEntry(appId, attemptCount));
|
||||
AndroidAlarmManager.oneShot(Duration(seconds: remainingSeconds),
|
||||
taskId + 1, bgUpdateCheck,
|
||||
params: {
|
||||
'toCheck': remainingToCheck
|
||||
.map(
|
||||
(entry) => {'key': entry.key, 'value': entry.value})
|
||||
.toList(),
|
||||
'toInstall': toInstall
|
||||
.map(
|
||||
(entry) => {'key': entry.key, 'value': entry.value})
|
||||
.toList(),
|
||||
});
|
||||
break;
|
||||
} else {
|
||||
// If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
|
||||
toCheck.removeAt(i);
|
||||
i--;
|
||||
notificationsProvider
|
||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||
}
|
||||
} finally {
|
||||
if (notif != null) {
|
||||
notificationsProvider.cancel(notif.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (toNotify.isNotEmpty) {
|
||||
notificationsProvider.notify(UpdateNotification(toNotify));
|
||||
}
|
||||
}
|
||||
// If you're done checking and found some silently installable updates, schedule another task which will run in install mode
|
||||
if (didCompleteChecking && toInstall.isNotEmpty) {
|
||||
logs.add(
|
||||
'BG update task $taskId: Done. Scheduling install task to run immediately.');
|
||||
AndroidAlarmManager.oneShot(
|
||||
const Duration(minutes: 0), taskId + 1, bgUpdateCheck,
|
||||
params: {
|
||||
'toCheck': [],
|
||||
'toInstall': toInstall
|
||||
.map((entry) => {'key': entry.key, 'value': entry.value})
|
||||
.toList()
|
||||
});
|
||||
} else if (didCompleteChecking) {
|
||||
logs.add('BG install task $taskId: Done.');
|
||||
}
|
||||
} else {
|
||||
// If in install mode...
|
||||
var didCompleteInstalling = false;
|
||||
var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
|
||||
if (tempObtArr.isNotEmpty) {
|
||||
// Move obtainium to the end of the list as it must always install last
|
||||
var obt = tempObtArr.first;
|
||||
toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
|
||||
}
|
||||
// Loop through all updates and install each
|
||||
for (var i = 0; i < toInstall.length; i++) {
|
||||
var appId = toInstall[i].key;
|
||||
var retryCount = toInstall[i].value;
|
||||
try {
|
||||
logs.add(
|
||||
'BG install task $taskId: Attempting to update $appId in the background.');
|
||||
await appsProvider.downloadAndInstallLatestApps([appId], null,
|
||||
notificationsProvider: notificationsProvider);
|
||||
await Future.delayed(const Duration(
|
||||
seconds:
|
||||
5)); // Just in case task ending causes install fail (not clear)
|
||||
if (i == (toCheck.length - 1)) {
|
||||
didCompleteInstalling = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly
|
||||
logs.add(
|
||||
'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.');
|
||||
if (retryCount < maxAttempts) {
|
||||
var remainingSeconds = retryCount;
|
||||
logs.add(
|
||||
'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
|
||||
var remainingToInstall = moveStrToEndMapEntryWithCount(
|
||||
toInstall.sublist(i), MapEntry(appId, retryCount + 1));
|
||||
AndroidAlarmManager.oneShot(
|
||||
Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck,
|
||||
params: {
|
||||
'toCheck': toCheck
|
||||
.map((entry) => {'key': entry.key, 'value': entry.value})
|
||||
.toList(),
|
||||
'toInstall': remainingToInstall
|
||||
.map((entry) => {'key': entry.key, 'value': entry.value})
|
||||
.toList(),
|
||||
});
|
||||
break;
|
||||
} else {
|
||||
// If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
|
||||
toInstall.removeAt(i);
|
||||
i--;
|
||||
notificationsProvider
|
||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||
}
|
||||
}
|
||||
if (didCompleteInstalling) {
|
||||
logs.add('BG install task $taskId: Done.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ class ObtainiumNotification {
|
||||
}
|
||||
|
||||
class UpdateNotification extends ObtainiumNotification {
|
||||
UpdateNotification(List<App> updates)
|
||||
UpdateNotification(List<App> updates, {int? id})
|
||||
: super(
|
||||
2,
|
||||
id ?? 2,
|
||||
tr('updatesAvailable'),
|
||||
'',
|
||||
'UPDATES_AVAILABLE',
|
||||
@ -41,8 +41,8 @@ class UpdateNotification extends ObtainiumNotification {
|
||||
}
|
||||
|
||||
class SilentUpdateNotification extends ObtainiumNotification {
|
||||
SilentUpdateNotification(List<App> updates)
|
||||
: super(3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'),
|
||||
SilentUpdateNotification(List<App> updates, {int? id})
|
||||
: super(id ?? 3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'),
|
||||
tr('appsUpdatedNotifDescription'), Importance.defaultImportance) {
|
||||
message = updates.length == 1
|
||||
? tr('xWasUpdatedToY',
|
||||
@ -52,10 +52,28 @@ class SilentUpdateNotification extends ObtainiumNotification {
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||
ErrorCheckingUpdatesNotification(String error)
|
||||
class SilentUpdateAttemptNotification extends ObtainiumNotification {
|
||||
SilentUpdateAttemptNotification(List<App> updates, {int? id})
|
||||
: super(
|
||||
5,
|
||||
id ?? 3,
|
||||
tr('appsPossiblyUpdated'),
|
||||
'',
|
||||
'APPS_POSSIBLY_UPDATED',
|
||||
tr('appsPossiblyUpdated'),
|
||||
tr('appsPossiblyUpdatedNotifDescription'),
|
||||
Importance.defaultImportance) {
|
||||
message = updates.length == 1
|
||||
? tr('xWasPossiblyUpdatedToY',
|
||||
args: [updates[0].finalName, updates[0].latestVersion])
|
||||
: plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1,
|
||||
args: [updates[0].finalName, (updates.length - 1).toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||
ErrorCheckingUpdatesNotification(String error, {int? id})
|
||||
: super(
|
||||
id ?? 5,
|
||||
tr('errorCheckingUpdates'),
|
||||
error,
|
||||
'BG_UPDATE_CHECK_ERROR',
|
||||
@ -99,14 +117,17 @@ final completeInstallationNotification = ObtainiumNotification(
|
||||
tr('completeAppInstallationNotifDescription'),
|
||||
Importance.max);
|
||||
|
||||
final checkingUpdatesNotification = ObtainiumNotification(
|
||||
class CheckingUpdatesNotification extends ObtainiumNotification {
|
||||
CheckingUpdatesNotification(String appName)
|
||||
: super(
|
||||
4,
|
||||
tr('checkingForUpdates'),
|
||||
'',
|
||||
appName,
|
||||
'BG_UPDATE_CHECK',
|
||||
tr('checkingForUpdates'),
|
||||
tr('checkingForUpdatesNotifDescription'),
|
||||
Importance.min);
|
||||
}
|
||||
|
||||
class NotificationsProvider {
|
||||
FlutterLocalNotificationsPlugin notifications =
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
@ -309,4 +313,97 @@ class SettingsProvider with ChangeNotifier {
|
||||
prefs?.setBool('reversePageTransitions', show);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get enableBackgroundUpdates {
|
||||
return prefs?.getBool('enableBackgroundUpdates') ?? true;
|
||||
}
|
||||
|
||||
set enableBackgroundUpdates(bool val) {
|
||||
prefs?.setBool('enableBackgroundUpdates', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get bgUpdatesOnWiFiOnly {
|
||||
return prefs?.getBool('bgUpdatesOnWiFiOnly') ?? false;
|
||||
}
|
||||
|
||||
set bgUpdatesOnWiFiOnly(bool val) {
|
||||
prefs?.setBool('bgUpdatesOnWiFiOnly', val);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
DateTime get lastBGCheckTime {
|
||||
int? temp = prefs?.getInt('lastBGCheckTime');
|
||||
return temp != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(temp)
|
||||
: DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
set lastBGCheckTime(DateTime val) {
|
||||
prefs?.setInt('lastBGCheckTime', val.millisecondsSinceEpoch);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get showDebugOpts {
|
||||
return prefs?.getBool('showDebugOpts') ?? false;
|
||||
}
|
||||
|
||||
set showDebugOpts(bool val) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,13 @@ import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||
import 'package:obtainium/app_sources/apkpure.dart';
|
||||
import 'package:obtainium/app_sources/aptoide.dart';
|
||||
import 'package:obtainium/app_sources/codeberg.dart';
|
||||
import 'package:obtainium/app_sources/fdroid.dart';
|
||||
import 'package:obtainium/app_sources/fdroidrepo.dart';
|
||||
import 'package:obtainium/app_sources/github.dart';
|
||||
import 'package:obtainium/app_sources/gitlab.dart';
|
||||
import 'package:obtainium/app_sources/huaweiappgallery.dart';
|
||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||
import 'package:obtainium/app_sources/html.dart';
|
||||
import 'package:obtainium/app_sources/jenkins.dart';
|
||||
@ -24,10 +26,12 @@ 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';
|
||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||
import 'package:obtainium/providers/settings_provider.dart';
|
||||
|
||||
class AppNames {
|
||||
late String author;
|
||||
@ -323,21 +327,29 @@ abstract class AppSource {
|
||||
bool enforceTrackOnly = false;
|
||||
bool changeLogIfAnyIsMarkDown = true;
|
||||
bool appIdInferIsOptional = false;
|
||||
bool allowSubDomains = false;
|
||||
|
||||
AppSource() {
|
||||
name = runtimeType.toString();
|
||||
}
|
||||
|
||||
overrideVersionDetectionFormDefault(String vd, bool disableStandard) {
|
||||
overrideVersionDetectionFormDefault(String vd,
|
||||
{bool disableStandard = false, bool disableRelDate = false}) {
|
||||
additionalAppSpecificSourceAgnosticSettingFormItems =
|
||||
additionalAppSpecificSourceAgnosticSettingFormItems.map((e) {
|
||||
return e.map((e2) {
|
||||
if (e2.key == 'versionDetection') {
|
||||
var item = e2 as GeneratedFormDropdown;
|
||||
item.defaultValue = vd;
|
||||
item.disabledOptKeys = [];
|
||||
if (disableStandard) {
|
||||
item.disabledOptKeys = ['standardVersionDetection'];
|
||||
item.disabledOptKeys?.add('standardVersionDetection');
|
||||
}
|
||||
if (disableRelDate) {
|
||||
item.disabledOptKeys?.add('releaseDateAsVersion');
|
||||
}
|
||||
item.disabledOptKeys =
|
||||
item.disabledOptKeys?.where((element) => element != vd).toList();
|
||||
}
|
||||
return e2;
|
||||
}).toList();
|
||||
@ -352,12 +364,24 @@ 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) async {
|
||||
if (requestHeaders != null) {
|
||||
Future<Response> sourceRequest(String url,
|
||||
{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.headers.addAll(requestHeaders!);
|
||||
req.followRedirects = followRedirects;
|
||||
if (requestHeaders != null) {
|
||||
req.headers.addAll(requestHeaders);
|
||||
}
|
||||
return Response.fromStream(await Client().send(req));
|
||||
} else {
|
||||
return get(Uri.parse(url));
|
||||
@ -412,7 +436,11 @@ abstract class AppSource {
|
||||
GeneratedFormSwitch('autoApkFilterByArch',
|
||||
label: tr('autoApkFilterByArch'), defaultValue: true)
|
||||
],
|
||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)]
|
||||
[GeneratedFormTextField('appName', label: tr('appName'), required: false)],
|
||||
[
|
||||
GeneratedFormSwitch('exemptFromBackgroundUpdates',
|
||||
label: tr('exemptFromBackgroundUpdates'))
|
||||
]
|
||||
];
|
||||
|
||||
// Previous 2 variables combined into one at runtime for convenient usage
|
||||
@ -424,12 +452,31 @@ abstract class AppSource {
|
||||
}
|
||||
|
||||
// Some Sources may have additional settings at the Source level (not specific to Apps) - these use SettingsProvider
|
||||
List<GeneratedFormItem> additionalSourceSpecificSettingFormItems = [];
|
||||
// If the source has been overridden, we expect the user to define one-time values as additional settings - don't use the stored values
|
||||
List<GeneratedFormItem> sourceConfigSettingFormItems = [];
|
||||
Future<Map<String, String>> getSourceConfigValues(
|
||||
Map<String, dynamic> additionalSettings,
|
||||
SettingsProvider settingsProvider) async {
|
||||
Map<String, String> results = {};
|
||||
for (var e in sourceConfigSettingFormItems) {
|
||||
var val = hostChanged
|
||||
? additionalSettings[e.key]
|
||||
: settingsProvider.getSettingString(e.key);
|
||||
if (val != null) {
|
||||
results[e.key] = val;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
String? changeLogPageFromStandardUrl(String standardUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> getSourceNote() async {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> apkUrlPrefetchModifier(
|
||||
String apkUrl, String standardUrl) async {
|
||||
return apkUrl;
|
||||
@ -474,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 => [
|
||||
@ -481,13 +542,16 @@ class SourceProvider {
|
||||
GitLab(),
|
||||
Codeberg(),
|
||||
FDroid(),
|
||||
IzzyOnDroid(),
|
||||
FDroidRepo(),
|
||||
Jenkins(),
|
||||
IzzyOnDroid(),
|
||||
SourceForge(),
|
||||
SourceHut(),
|
||||
APKMirror(),
|
||||
APKPure(),
|
||||
Aptoide(),
|
||||
Uptodown(),
|
||||
APKMirror(),
|
||||
HuaweiAppGallery(),
|
||||
Jenkins(),
|
||||
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||
Mullvad(),
|
||||
Signal(),
|
||||
@ -517,7 +581,9 @@ class SourceProvider {
|
||||
}
|
||||
AppSource? source;
|
||||
for (var s in sources.where((element) => element.host != null)) {
|
||||
if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) {
|
||||
if (RegExp(
|
||||
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?')
|
||||
.hasMatch(url)) {
|
||||
source = s;
|
||||
break;
|
||||
}
|
||||
|
298
pubspec.lock
298
pubspec.lock
@ -5,43 +5,51 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_alarm_manager_plus
|
||||
sha256: "80f963d47cb7ab0818144c7b0668aea4c038f9cb8626626e89a4ea77375defb7"
|
||||
sha256: c20d91a9096596f66274bf8172321c278f9cba8091638f80205fe66d31587fa5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_intent_plus
|
||||
sha256: "2c87d8330ba5deef5fe20e77f4d178190b3b24531dce08368030ab4be40a9d4e"
|
||||
sha256: f72ae20bb37108694f442e7ae6acbd28b453ca62ce86842f6787b784355abfe6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
version: "4.0.2"
|
||||
android_package_installer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1
|
||||
resolved-ref: ba2aa7a11edc2649d1d80c25ed9291521262f714
|
||||
url: "https://github.com/ImranR98/android_package_installer"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
android_package_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_package_manager
|
||||
sha256: b873fe5856f7c442aca9751dac05d117285be9e4de08eb15d1ffb811fd1b688d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animations
|
||||
sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164
|
||||
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||
sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.7"
|
||||
version: "3.3.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -102,10 +110,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.1"
|
||||
version: "1.17.2"
|
||||
connectivity_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -118,10 +142,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
|
||||
sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
version: "0.3.3+5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -142,10 +166,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -158,10 +182,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "2c35b6d1682b028e42d07b3aee4b98fa62996c10bc12cb651ec856a80d6a761b"
|
||||
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.2"
|
||||
version: "9.0.3"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -174,18 +198,18 @@ 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:
|
||||
name: easy_localization
|
||||
sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645"
|
||||
sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
easy_logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -206,10 +230,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -222,10 +246,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a"
|
||||
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.3"
|
||||
version: "5.5.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -259,18 +283,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975"
|
||||
sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.1.0+1"
|
||||
version: "15.1.1"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -296,18 +320,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "4b1bfbb802d76320a1a46d9ce984106135093efd9d969765d07c2125af107bdf"
|
||||
sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.17"
|
||||
version: "0.6.17+3"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
|
||||
sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.0.16"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -366,22 +390,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.17"
|
||||
installed_apps:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: installed_apps
|
||||
sha256: "145af8eb6e4e7c830e9888d6de0573ae5c139e8e0742a3e67316e1db21ab6fe0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.0"
|
||||
version: "0.18.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -418,18 +434,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.15"
|
||||
version: "0.12.16"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.5.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -454,22 +470,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_archive_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_archive_info
|
||||
sha256: "8f671a29b79d15f192e5e2b0dab9d0bad66b9ee93fb58d4e0afdb62f91a259be"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
package_info:
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info
|
||||
sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75"
|
||||
name: nm
|
||||
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "0.5.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -482,66 +490,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.1.1"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.2.0"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.3.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.2.1"
|
||||
permission_handler:
|
||||
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: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221"
|
||||
sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.3"
|
||||
version: "11.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -554,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:
|
||||
@ -578,18 +586,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -610,74 +618,82 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: ed3fcea4f789ed95913328e629c0c53e69e80e08b6c24542f1b3576046c614e8
|
||||
sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
version: "7.1.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
|
||||
sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.3.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
|
||||
sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
|
||||
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4
|
||||
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||
sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
|
||||
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
|
||||
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||
sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
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
|
||||
@ -687,26 +703,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.10.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
|
||||
sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.8+4"
|
||||
version: "2.3.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f"
|
||||
sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.5+1"
|
||||
version: "2.5.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -751,10 +767,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "0.6.0"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -775,66 +791,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.12"
|
||||
version: "6.1.14"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af"
|
||||
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.37"
|
||||
version: "6.1.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
|
||||
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.6"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
|
||||
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
|
||||
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.5"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
|
||||
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.18"
|
||||
version: "2.0.20"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
|
||||
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.0.8"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -851,46 +867,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
webview_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00"
|
||||
sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
version: "4.2.4"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "8587d0b4991bd0f223f4b4957101c2c7449f905601571315f4967072498dd3fb"
|
||||
sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
version: "3.10.1"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30"
|
||||
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.6.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "3e36a8f564809cb7c257ff4278502b185e2191349df0ddee98837f91805c74b8"
|
||||
sha256: d2f7241849582da80b79acb03bb936422412ce5c0c79fb5f6a1de5421a5aecc4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.1"
|
||||
version: "3.7.4"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee
|
||||
sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
version: "5.0.7"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -903,10 +927,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -924,5 +948,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
dart: ">=3.1.0 <4.0.0"
|
||||
flutter: ">=3.13.0"
|
||||
|
13
pubspec.yaml
13
pubspec.yaml
@ -1,5 +1,5 @@
|
||||
name: obtainium
|
||||
description: A new Flutter project.
|
||||
description: Get Android App Updates Directly From the Source.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
@ -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.13.21+185 # 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
|
||||
@ -55,9 +55,8 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/ImranR98/android_package_installer
|
||||
ref: main
|
||||
android_package_manager: ^0.6.0
|
||||
share_plus: ^7.0.0
|
||||
installed_apps: ^1.3.1
|
||||
package_archive_info: ^0.1.0
|
||||
android_alarm_manager_plus: ^3.0.0
|
||||
sqflite: ^2.2.0+3
|
||||
easy_localization: ^3.0.1
|
||||
@ -65,6 +64,8 @@ dependencies:
|
||||
flutter_markdown: ^0.6.14
|
||||
flutter_archive: ^5.0.0
|
||||
hsluv: ^1.1.3
|
||||
connectivity_plus: ^4.0.2
|
||||
shared_storage: ^0.8.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user