Compare commits

...

139 Commits

Author SHA1 Message Date
71543c64d4 Merge pull request #823 from ImranR98/dev
Various Enhancements and Bugfixes
2023-08-28 20:27:03 -04:00
408c1a541f Increment version 2023-08-28 20:26:21 -04:00
8f739d9e0b WiFi only BG update toggle (#819) + typo fix 2023-08-28 20:24:15 -04:00
bd8f608ee6 Allow apps to be exempted from BG updates (#822) 2023-08-28 20:02:09 -04:00
551643b11c More UI feedback when app updated (#814) 2023-08-28 19:51:49 -04:00
075ecae540 Re-add VLC with better error messaging when no mirror available (#821) 2023-08-28 19:36:02 -04:00
dcb8807fa7 Add "Intermediate Link" option to HTML Source (#820) 2023-08-28 19:11:22 -04:00
77ec2df31c Merge pull request #818 from LucasTavaresA/main
Add brasilian translation
2023-08-27 17:45:19 -04:00
d92e812554 Add brasilian translation 2023-08-27 18:27:09 -03:00
0a6e1f9cc6 Switch back to upstream package manager (from custom branch) 2023-08-27 15:43:17 -04:00
f3c3680382 Merge pull request #817 from ImranR98/dev
Increment version
2023-08-27 14:27:13 -04:00
f7e783a556 Increment version 2023-08-27 14:26:37 -04:00
fd1b72563d Merge pull request #816 from ImranR98/dev
Remove VLC (#801)
2023-08-27 14:25:11 -04:00
5a94ef82dd Merge remote-tracking branch 'origin/main' into dev 2023-08-27 14:24:59 -04:00
3ad46b7e21 Merge pull request #803 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-27 14:23:59 -04:00
1e94d71665 locale(pl): Update Polish translations 2023-08-27 19:59:19 +02:00
0899a576ff Merge pull request #815 from bluefly000/japanese-translation
Update ja.json
2023-08-27 13:21:42 -04:00
db2476f3a5 Update ja.json 2023-08-28 01:36:14 +09:00
8ba182870d Merge pull request #812 from LilligantMatsuri/main
Update Chinese translation
2023-08-27 07:48:04 -04:00
d81085a9e8 Update zh.json
- Translate new strings
- Slight improvements

Signed-off-by: Matsuri <matsuri@vmoe.info>
2023-08-27 15:30:01 +08:00
ae92a459b7 Remove VLC (#801) 2023-08-26 15:35:08 -04:00
e101c434d5 Merge pull request #809 from bluefly000/japanese-translation
Update ja.json
2023-08-26 14:54:48 -04:00
9686d0f0ca Added missing enableBackgroundUpdates text 2023-08-27 01:20:51 +09:00
1dd0392b78 Update ja.json 2023-08-27 01:15:08 +09:00
5e184d733b Merge pull request #806 from ImranR98/dev
Update README
2023-08-25 21:01:29 -04:00
ef0b20887b Update README 2023-08-25 21:00:57 -04:00
5cb4bd998b Merge pull request #805 from ImranR98/dev
Update packages, increment version
2023-08-25 20:54:18 -04:00
57f499c6a5 Update packages, increment version 2023-08-25 20:53:23 -04:00
c2ae6e19f9 Merge pull request #804 from ImranR98/dev
'Verify latest tag' toggle (#798, #740)
2023-08-25 20:41:59 -04:00
16104fde03 Merge branch 'main' into dev 2023-08-25 20:41:52 -04:00
13e10692b1 'Verify latest tag' toggle (#798, #740) 2023-08-25 20:40:35 -04:00
7fc93f23c0 Merge pull request #802 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-25 17:01:09 -04:00
d6ddf87365 locale(pl): Update Polish translations
Polish translation has been updated

Signed-off-by: Daviteusz <daviteusz0@gmail.com>
2023-08-25 22:49:54 +02:00
05172744cd Merge pull request #800 from ImranR98/dev
Enable Background Updates (#25)
2023-08-25 10:54:38 -04:00
2504ae24fc Fix bgcheck error reporting (per-app), move code around 2023-08-24 15:05:07 -04:00
5e41d5762b BG task: notif, retry, log tweaks 2023-08-24 10:46:11 -04:00
57d44c972f Update build script to create zips 2023-08-23 20:05:08 -04:00
be0b57ac00 Added logs, BG install cooldown 2023-08-23 19:51:52 -04:00
862bb2b276 chmod +x build.sh 2023-08-23 19:39:23 -04:00
5eac851f80 Added convenience build script 2023-08-23 19:36:24 -04:00
0deab8296f bug 2023-08-23 18:28:16 -04:00
6785708661 BG update toggle has an effect 2023-08-22 19:57:42 -04:00
5307fd0901 bug 2023-08-22 19:43:23 -04:00
788c4c7917 Try less messy bg update method 2023-08-22 19:14:56 -04:00
3f6d96289d Bugfix, logging 2023-08-22 18:13:28 -04:00
5cfd80e510 Added debug menu with on-demand bg task 2023-08-22 17:36:13 -04:00
9eb32ae55a Don't reschedule bg checks if app is restarted 2023-08-22 17:13:15 -04:00
82e08150ab bugs 2023-08-22 16:28:22 -04:00
e956ee9254 Trying a new recursive BG update task due2 mem limits 2023-08-22 12:51:55 -04:00
bb4f34317b Bugfix + version increment + update packages 2023-08-21 23:55:54 -04:00
d08ff3fbae Add BG update toggle 2023-08-21 20:32:41 -04:00
f5479ec82f Max 5 attempts for bg check fail due to rate/net 2023-08-21 20:15:57 -04:00
3eb25c4060 Switch to per-app BG update tasks 2023-08-21 20:10:30 -04:00
03bb1ad9a6 bugfix 2023-08-21 09:48:35 -04:00
b59d3e77f9 Enable experimental BG update support (not well tested) (#25) 2023-08-20 22:32:33 -04:00
7c41692d5f Enable version correction in background (replace plugin) 2023-08-20 16:03:41 -04:00
ce89d456e1 Merge pull request #788 from ImranR98/dev
Pub update
2023-08-19 01:44:36 -04:00
a0c48fcca6 Pub update 2023-08-19 01:44:02 -04:00
d0cba6d6bc Merge pull request #787 from ImranR98/dev
Enable Android TV 'OK' Button (#281), Add Huawei AppGallery (#756), Fix VLC Source (#758)
2023-08-19 01:41:31 -04:00
6baf6ccf4b Slightly better error reporting for failed xapk install 2023-08-19 01:40:14 -04:00
a2571e61a8 Fix VLC Source (#758) 2023-08-19 01:28:25 -04:00
f61824ff0d Add Huawei AppGallery (#756) 2023-08-19 00:19:52 -04:00
734a1aeb01 Enable Android TV 'OK' Button (#281) 2023-08-18 22:12:16 -04:00
5269aad90d Merge pull request #786 from ImranR98/dev
Don't Send "Foreground" Notification if Not Needed + Small UI Fix
2023-08-18 20:11:34 -04:00
eaeee188eb Upgrade packages, increment version 2023-08-18 19:58:54 -04:00
8c850e06ca Merge pull request #774 from iDazai/main
Update de.json
2023-08-18 19:56:59 -04:00
0f754a8da8 Merge pull request #775 from bluefly000/japanese-translation
Update ja.json
2023-08-18 19:56:31 -04:00
81c4d4f393 Fix default multi-"app change" selection bug 2023-08-18 19:55:31 -04:00
5317aee18d Merge pull request #776 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-18 19:54:44 -04:00
b66eeba3b5 Merge pull request #781 from mehdeej/main
Update fa.json
2023-08-18 19:54:22 -04:00
522ff1ddf7 Merge pull request #784 from Nriver/task/update-chinese-translation
update chinese translation
2023-08-18 19:54:06 -04:00
7ef9c43ee3 Don't wait for foreground if install is silent 2023-08-18 19:36:56 -04:00
05ac76e3e9 update chinese translation 2023-08-18 16:56:20 +08:00
4838402797 Update fa.json 2023-08-16 18:54:58 +00:00
0de12c7c07 locale(pl): Update Polish translations
Polish translation has been updated

Signed-off-by: Daviteusz <daviteusz0@gmail.com>
2023-08-14 23:24:16 +02:00
0dadd8bffe Update ja.json 2023-08-15 02:57:31 +09:00
75a0cb1189 Update de.json
translated newly added line
2023-08-14 14:38:38 +02:00
a52d936b4d Merge pull request #773 from ImranR98/dev
Increment version
2023-08-13 21:28:02 -04:00
cfd04dc602 Increment version 2023-08-13 21:27:41 -04:00
7622e63975 Merge pull request #754 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-13 21:26:51 -04:00
3e7172b9d1 Merge branch 'main' into weblate-obtainium-translate 2023-08-13 21:26:44 -04:00
7aa0294447 Merge pull request #760 from iDazai/main
Update de.json
2023-08-13 21:26:14 -04:00
5b89c4d293 Merge branch 'main' into main 2023-08-13 21:26:08 -04:00
3bb991f57f Merge pull request #765 from Marocco2/main-1
Update html user agent with a believable one
2023-08-13 21:25:47 -04:00
1dd3aa0e8a Merge pull request #766 from Octopus1348/main
Make Hungarian translation more understandable
2023-08-13 21:25:41 -04:00
189cecbc37 Merge branch 'main' into main 2023-08-13 21:25:35 -04:00
9680ba08e9 Merge pull request #772 from ImranR98/dev
Use custom link filters for HTML, very basic foreground-only silent updates when able
2023-08-13 21:24:35 -04:00
dcf5bd37ca Foreground-only silent install support 2023-08-13 21:18:53 -04:00
d6a4b0a96d Make Hungarian translation more understandable 2023-08-11 18:56:25 +02:00
5f8638d349 Update html.dart
Swap user agent with a chrome browser on Android
2023-08-11 17:27:12 +02:00
1de274df39 Update de.json
translated the two new added lines, and one yet to be translated string.
2023-08-09 08:15:07 +02:00
3df801c54e locale(pl): Update Polish translations
Polish translation has been updated

Signed-off-by: Daviteusz <daviteusz0@gmail.com>
2023-08-06 22:49:52 +02:00
d473cb49c5 Add custom link filter for HTML 2023-08-06 13:14:48 -04:00
2a5118a2cf Merge pull request #749 from ImranR98/dev
- Fix a French word (#735)
- Add filename-only sort for HTML (#734)
- Add dynamic mirror picking to VLC (#715)
- Disable reverse transition method when n/a (#739)
- Make delete confirmation button red (#741)
- Add release notes filter for GitHub/Codeberg (#719)
2023-08-05 14:31:35 -04:00
347c56da55 Ran dart fix 2023-08-05 14:29:42 -04:00
8073723e1f Increment version, update packages 2023-08-05 14:28:40 -04:00
c8e90a755d Add release notes filter for GitHub/Codeberg(#719) 2023-08-05 14:23:23 -04:00
aeb0a5d8ea Make delete confirmation button red (#741) 2023-08-05 14:12:03 -04:00
09fe7f3ecd Disable reverse transition method when n/a (#739) 2023-08-05 14:00:50 -04:00
a549411589 Add dynamic mirror picking to VLC (#715) 2023-08-05 13:55:25 -04:00
f426b5e118 Add filename-only sort for HTML (#734) 2023-08-05 12:52:08 -04:00
12a8ef5e31 Fix a French word (#735) 2023-08-05 12:39:58 -04:00
8210279a4c Merge pull request #728 from TangyWrecker/main
Update ru.json
2023-08-05 12:37:35 -04:00
999d13b80d Merge pull request #731 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-08-05 12:37:30 -04:00
0573c0b270 Merge pull request #733 from bluefly000/japanese-translation
Update ja.json
2023-08-05 12:37:25 -04:00
5a36a7980c Merge pull request #736 from gidano/main
Update hu.json
2023-08-05 12:37:18 -04:00
6bf9b5297f Merge pull request #746 from vaginessa/main
Update de.json - translate missing strings
2023-08-05 12:37:12 -04:00
fdc6b0ff00 Merge pull request #748 from LilligantMatsuri/main
Update Chinese translation
2023-08-05 12:37:05 -04:00
73c20a53d2 Update zh.json
- Translate new strings
- Slight improvements

Signed-off-by: Matsuri <matsuri@vmoe.info>
2023-08-05 23:27:13 +08:00
16ae8d8e4d Update de.json - translate missing strings 2023-08-05 15:53:09 +02:00
3f8cfae64e Update hu.json 2023-08-01 16:28:31 +02:00
6861a71efb Update ja.json 2023-07-31 19:52:12 +09:00
2a603f410f locale(pl): Update Polish translations
Co-authored-by: Daviteusz <daviteusz0@gmail.com>
2023-07-30 16:28:42 +02:00
6d22788f92 Update ru.json 2023-07-30 13:03:14 +03:00
e36e6bbaca Merge pull request #713 from Daviteusz/weblate-obtainium-translate
locale(pl): Update Polish translation
2023-07-29 22:44:20 -04:00
ac3a13ed73 Merge branch 'main' into weblate-obtainium-translate 2023-07-29 22:44:13 -04:00
202f7df5cb Merge pull request #724 from markus-gitdev/patch-1
Update de.json
2023-07-29 22:43:53 -04:00
1b902b1a18 Merge branch 'main' into patch-1 2023-07-29 22:43:45 -04:00
1765d399c8 Merge pull request #726 from ImranR98/dev
Use App-Specific Source Config for Overridden Sources, Show Source Config Notes/Hints (#720), Bugfixes and UI tweaks (#714, #722, #723)
2023-07-29 22:42:58 -04:00
5dd79707f1 Update packages, increment version 2023-07-29 22:40:52 -04:00
14755134bf Use icon button for delete on app page (#722) 2023-07-29 22:39:50 -04:00
cccde7e135 UI bugfix (#723) 2023-07-29 22:29:08 -04:00
3dafd643c0 Only show host+path for ClientException log (#714) 2023-07-29 22:21:07 -04:00
76f8cd4102 Source configuration settings changes
- "Source config" refers to source-specific, app-agnostic settings
- Don't use saved config for overridden sources
- For overridden sources, use app-specific source config
- Allow sources to show notes on add-app page (#720)
2023-07-29 22:06:42 -04:00
a8bfb03f58 Wait 5s between download retries 2023-07-29 20:22:04 -04:00
10ead4f3e0 Update de.json 2023-07-29 14:44:21 +02:00
af5a6857ba locale(pl): Update Polish translations
Co-authored-by: Daviteusz <daviteusz0@gmail.com>
2023-07-24 23:42:11 +02:00
d9225fd639 Merge pull request #712 from ImranR98/dev
Auto retry failed download (3 times) (#634)
2023-07-23 15:03:34 -04:00
995d44551c Auto retry failed download (3 times) (#634) 2023-07-23 15:02:58 -04:00
68c0224b98 Merge pull request #711 from ImranR98/dev
Increment version, update modules
2023-07-23 14:26:48 -04:00
7767468d5d Increment version, update modules 2023-07-23 14:26:22 -04:00
f9f83d8243 Merge pull request #710 from ImranR98/dev
Fix empty error messages for HTTP errors
2023-07-23 14:24:50 -04:00
cc4cec829f Merge pull request #705 from gidano/main
Update hu.json
2023-07-23 14:24:23 -04:00
f665bf1eb2 Merge pull request #706 from TangyWrecker/main
Update ru.json
2023-07-23 14:24:18 -04:00
bccd52054e Merge pull request #708 from LilligantMatsuri/main
Update Chinese translation
2023-07-23 14:24:12 -04:00
3a4c782aab Fix empty error messages for HTTP errors 2023-07-23 14:23:34 -04:00
6e047e96fa Update Chinese translation
- Translate new strings

Signed-off-by: Matsuri <matsuri@vmoe.info>
2023-07-23 19:56:23 +08:00
42f8753166 Update ru.json 2023-07-23 14:34:15 +03:00
b6a64129b3 Update hu.json 2023-07-23 08:52:14 +02:00
37 changed files with 1886 additions and 649 deletions

View File

@ -18,11 +18,11 @@ Currently supported App sources:
- [SourceHut](https://git.sr.ht/)
- [APKMirror](https://apkmirror.com/) (Track-Only)
- [APKPure](https://apkpure.com/)
- [Huawei AppGallery](https://appgallery.huawei.com/)
- Third Party F-Droid Repos
- Jenkins Jobs
- [Steam](https://store.steampowered.com/mobile)
- [Telegram App](https://telegram.org)
- [VLC](https://www.videolan.org/vlc/download-android.html)
- [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)
@ -33,8 +33,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

View File

@ -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.

View File

@ -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>

303
assets/translations/br.json Normal file
View File

@ -0,0 +1,303 @@
{
"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)",
"githubPATHint": "O TAP deve estar nesse formato: usuario:token",
"githubPATFormat": "usuario:token",
"includePrereleases": "Incluir pré-lançamentos",
"fallbackToOlderReleases": "Retornar para versões anteriores",
"filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
"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 {}",
"supportedSourcesBelow": "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",
"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."
}
}

View File

@ -1,4 +1,4 @@
{
{
"invalidURLForSource": "Nije važeći URL aplikacije {}",
"noReleaseFound": "Nije moguće pronaći odgovarajuće izdanje",
"noVersionFound": "Nije moguće odrediti verziju izdanja",
@ -11,12 +11,6 @@
"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",
@ -240,6 +234,21 @@
"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",
"removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?"
@ -287,5 +296,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."
}
}
}

View File

@ -11,12 +11,6 @@
"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",
@ -229,17 +223,32 @@
"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 Link Filter nach Regulärem Ausdruck (Standard '.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",
"removeAppQuestion": {
"one": "App entfernen?",
"other": "Apps entfernen?"
@ -287,5 +296,9 @@
"xAndNMoreUpdatesInstalled": {
"one": "{} und 1 weitere Anwendung wurden aktualisiert.",
"other": "{} und {} weitere Anwendungen wurden aktualisiert."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.",
"other": "{} and {} more apps may have been updated."
}
}

View File

@ -11,12 +11,6 @@
"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",
@ -240,6 +234,24 @@
"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",
"removeAppQuestion": {
"one": "Remove App?",
"other": "Remove Apps?"
@ -285,7 +297,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."
}
}

View File

@ -11,12 +11,6 @@
"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",
@ -240,6 +234,21 @@
"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",
"removeAppQuestion": {
"one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?"
@ -287,5 +296,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."
}
}

View File

@ -11,12 +11,6 @@
"unexpectedError": "خطای غیرمنتظره",
"ok": "باشه",
"and": "و",
"startedBgUpdateTask": "شروع بررسی بروزرسانی BG",
"bgUpdateIgnoreAfterIs": "نادیده گرفتن بروزرسانی BG بعد از {} است",
"startedActualBGUpdateCheck": "بررسی به‌روزرسانی واقعی BG آغاز شد",
"bgUpdateTaskFinished": "کار بررسی به‌روزرسانی BG تمام شد",
"firstRun": "این اولین اجرای Obtainium است",
"settingUpdateCheckIntervalTo": "تنظیم فاصله به‌روزرسانی روی {}",
"githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
"githubPATHint": "PAT باید در این قالب باشد: username:token",
"githubPATFormat": "username:token",
@ -229,17 +223,32 @@
"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",
"removeAppQuestion": {
"one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟"
@ -287,5 +296,9 @@
"xAndNMoreUpdatesInstalled": {
"one": "{} و 1 برنامه دیگر به روز شدند.",
"other": "{} و {} برنامه دیگر به روز شدند."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.",
"other": "{} and {} more apps may have been updated."
}
}

View File

@ -11,12 +11,6 @@
"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",
@ -45,7 +39,7 @@
"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 {}",
@ -240,6 +234,21 @@
"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",
"removeAppQuestion": {
"one": "Supprimer l'application ?",
"other": "Supprimer les applications ?"
@ -287,5 +296,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."
}
}

View File

@ -11,13 +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)",
"githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
"githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
"githubPATFormat": "felhasználónév:token",
"includePrereleases": "Tartalmazza az előzetes kiadásokat",
@ -93,13 +87,13 @@
"author": "Szerző",
"upToDateApps": "Naprakész appok",
"nonInstalledApps": "Nem telepített appok",
"importExport": "Import/Export",
"importExport": "Importálás/Exportálás",
"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,11 +133,11 @@
"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",
"pickAnAPK": "Válasszon egy APK-ot",
"appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:",
"deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.",
"deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:",
@ -210,7 +204,7 @@
"copiedToClipboard": "Másolva a vágólapra",
"storagePermissionDenied": "Tárhely engedély megtagadva",
"selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.",
"filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel",
"filterAPKsByRegEx": "Az APK-ok szűrése reguláris kifejezéssel",
"removeFromObtainium": "Eltávolítás az Obtainiumból",
"uninstallFromDevice": "Eltávolítás a készülékről",
"onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.",
@ -222,23 +216,38 @@
"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": "Disable page transition animations",
"reversePageTransitions": "Reverse page transition animations",
"minStarCount": "Minimum Star Count",
"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": "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",
"removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?"
@ -281,10 +290,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": "{} and 1 more app may have been updated.",
"other": "{} and {} more apps may have been updated."
}
}

View File

@ -11,12 +11,6 @@
"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",
@ -240,6 +234,21 @@
"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",
"removeAppQuestion": {
"one": "Rimuovere l'app?",
"other": "Rimuovere le app?"
@ -287,5 +296,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."
}
}

View File

@ -11,18 +11,12 @@
"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": "キャンセル",
@ -88,7 +82,7 @@
"showOutdatedOnly": "アップデートが存在するアプリのみ表示する",
"filter": "フィルター",
"filterActive": "フィルター *",
"filterApps": "アプリを絞り込む",
"filterApps": "アプリをフィルタリングする",
"appName": "アプリ名",
"author": "作者",
"upToDateApps": "最新のアプリ",
@ -211,7 +205,7 @@
"copiedToClipboard": "クリップボードにコピーしました",
"storagePermissionDenied": "ストレージ権限が拒否されました",
"selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。",
"filterAPKsByRegEx": "正規表現でAPKを絞り込む",
"filterAPKsByRegEx": "正規表現でAPKをフィルタリングする",
"removeFromObtainium": "Obtainiumから削除する",
"uninstallFromDevice": "デバイスからアンインストールする",
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
@ -239,14 +233,30 @@
"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'タグを確認する",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"
},
"tooManyRequestsTryAgainInMinutes": {
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
"one": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください"
},
"bgUpdateGotErrorRetryInMinutes": {
"one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します",
@ -257,28 +267,28 @@
"other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します"
},
"apps": {
"one": "{}個のアプリ",
"other": "{}個のアプリ"
"one": "{} 個のアプリ",
"other": "{} 個のアプリ"
},
"url": {
"one": "{}個のURL",
"other": "{}個のURL"
"one": "{} 個のURL",
"other": "{} 個のURL"
},
"minute": {
"one": "{}分",
"other": "{}分"
"one": "{} 分",
"other": "{} 分"
},
"hour": {
"one": "{}時間",
"other": "{}時間"
"one": "{} 時間",
"other": "{} 時間"
},
"day": {
"one": "{}日",
"other": "{}日"
"one": "{} 日",
"other": "{} 日"
},
"clearedNLogsBeforeXAfterY": {
"one": "{n}個のログをクリアしました (前 = {before}, 後 = {after})",
"other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})"
"one": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})",
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です",
@ -287,5 +297,9 @@
"xAndNMoreUpdatesInstalled": {
"one": "{} とさらに {} 個のアプリがアップデートされました",
"other": "{} とさらに {} 個のアプリがアップデートされました"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります",
"other": "{} とさらに {} 個のアプリがアップデートされた化膿性があります"
}
}

View File

@ -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,12 +21,6 @@
"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",
@ -243,49 +239,93 @@
"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": "Niestandardowy filtr linków wg. 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": "Exempt from background updates (if enabled)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
"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": "{} ap",
"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"
}

View File

@ -11,12 +11,6 @@
"unexpectedError": "Неожиданная ошибка",
"ok": "Окей",
"and": "и",
"startedBgUpdateTask": "Запущена задача фоновой проверки обновлений",
"bgUpdateIgnoreAfterIs": "Параметр игнорирования фоновых обновлений: {}",
"startedActualBGUpdateCheck": "Запущена фактическая проверка фоновых обновлений",
"bgUpdateTaskFinished": "Завершена задача фоновой проверки обновлений",
"firstRun": "Это первый запуск Obtainium",
"settingUpdateCheckIntervalTo": "Установка интервала проверки обновлений: {}",
"githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
"githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
"githubPATFormat": "имя_пользователя:токен",
@ -239,7 +233,22 @@
"checkUpdateOnDetailPage": "Проверять наличие обновлений при открытии страницы представления приложения",
"disablePageTransitions": "Отключить анимацию перехода между страницами",
"reversePageTransitions": "Реверс анимации перехода между страницами",
"minStarCount": "Minimum Star Count",
"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",
"removeAppQuestion": {
"one": "Удалить приложение?",
"other": "Удалить приложения?"
@ -287,5 +296,9 @@
"xAndNMoreUpdatesInstalled": {
"one": "{} и еще 1 приложение были обновлены.",
"other": "{} и еще {} приложений были обновлены."
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.",
"other": "{} and {} more apps may have been updated."
}
}

View File

@ -11,12 +11,6 @@
"unexpectedError": "意外错误",
"ok": "好的",
"and": "和",
"startedBgUpdateTask": "后台更新检查任务已启动",
"bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}",
"startedActualBGUpdateCheck": "开始后台更新检查",
"bgUpdateTaskFinished": "后台更新检查任务已完成",
"firstRun": "这是 Obtainium 首次启动",
"settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}",
"githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
"githubPATHint": "个人访问令牌必须为“username:token”的格式",
"githubPATFormat": "username:token",
@ -46,7 +40,7 @@
"appSourceURL": "来源 URL",
"error": "错误",
"add": "添加",
"searchSomeSourcesLabel": "搜索(仅部分来源)",
"searchSomeSourcesLabel": "搜索(仅支持部分来源)",
"search": "搜索",
"additionalOptsFor": "{} 的更多选项",
"supportedSourcesBelow": "支持的来源:",
@ -107,7 +101,7 @@
"searchX": "搜索 {}",
"noResults": "无结果",
"importX": "导入 {}",
"importedAppsIdDisclaimer": "导入的应用可能错误地显示为“未安装”。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
"importedAppsIdDisclaimer": "导入的应用可能错误地显示为“未安装”状态。\n请通过 Obtainium 重新安装这些应用来解决此问题。",
"importErrors": "导入错误",
"importedXOfYApps": "已导入 {} 中的 {} 个应用。",
"followingURLsHadErrors": "下列 URL 存在错误:",
@ -150,16 +144,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 +174,7 @@
"yesMarkUpdated": "是,标记为已更新",
"fdroid": "F-Droid 官方存储库",
"appIdOrName": "应用 ID 或名称",
"appId": "App ID",
"appId": "应用 ID",
"appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用",
"reposHaveMultipleApps": "存储库中可能包含多个应用",
"fdroidThirdPartyRepo": "F-Droid 第三方存储库",
@ -229,17 +223,33 @@
"dontShowTrackOnlyWarnings": "不显示“仅追踪”模式警告",
"dontShowAPKOriginWarnings": "不显示 APK 文件来源警告",
"moveNonInstalledAppsToBottom": "将未安装应用置底",
"gitlabPATLabel": "GitLab 个人访问令牌\n用于搜索应用 and Better APK Discovery",
"gitlabPATLabel": "GitLab 个人访问令牌\n启用搜索功能并增强 APK 发现",
"about": "相关文档",
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
"checkOnStart": "启动时进行一次检查",
"tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
"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": "使用访问令牌可避免触发 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",
"removeAppQuestion": {
"one": "是否删除应用?",
"other": "是否删除应用?"
@ -253,8 +263,8 @@
"other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试"
},
"bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "后台检查发现 {} 个应用更新 - 如有需要将发通知",
"other": "后台检查发现 {} 个应用更新 - 如有需要将发通知"
"one": "后台检查发现 {} 个应用更新 - 如有需要将发通知",
"other": "后台检查发现 {} 个应用更新 - 如有需要将发通知"
},
"apps": {
"one": "{} 个应用",
@ -287,5 +297,9 @@
"xAndNMoreUpdatesInstalled": {
"one": "{} 和另外 1 个应用已更新。",
"other": "{} 和另外 {} 个应用已更新。"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} 和另外 1 个应用已尝试更新。",
"other": "{} 和另外 {} 个应用已尝试更新。"
}
}

18
build.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Convenience script
CURR_DIR="$(pwd)"
trap "cd "$CURR_DIR"" EXIT
git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date
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/

View File

@ -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;

View File

@ -16,7 +16,7 @@ class GitHub extends AppSource {
host = 'github.com';
appIdInferIsOptional = true;
additionalSourceSpecificSettingFormItems = [
sourceConfigSettingFormItems = [
GeneratedFormTextField('github-creds',
label: tr('githubPATLabel'),
password: true,
@ -75,6 +75,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 +121,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 +169,30 @@ class GitHub extends AppSource {
return url.substring(0, match.end);
}
Future<String> getCredentialPrefixIfAny() async {
Future<String> getCredentialPrefixIfAny(
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['github-creds'];
return creds != null && creds.isNotEmpty ? '$creds@' : '';
}
Future<String> getAPIHost() async =>
'https://${await getCredentialPrefixIfAny()}api.$host';
@override
Future<String?> getSourceNote() async {
if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) {
return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
}
return null;
}
Future<String> convertStandardUrlToAPIUrl(String standardUrl) async =>
'${await getAPIHost()}/repos${standardUrl.substring('https://$host'.length)}';
Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
'https://${await getCredentialPrefixIfAny(additionalSettings)}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 +210,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>;
@ -231,6 +277,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 +310,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 +373,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 +422,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);

View File

@ -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);

View File

@ -1,6 +1,7 @@
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';
@ -85,11 +86,40 @@ 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)])
]
];
overrideVersionDetectionFormDefault('noVersionDetection',
disableStandard: true, disableRelDate: true);
}
@override
// TODO: implement requestHeaders choice, hardcoded for now
Map<String, String>? get requestHeaders => {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
};
@override
@ -105,14 +135,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'] ?? '')
.where((element) =>
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
.toList();
links.sort((a, b) => compareAlphaNumeric(a, b));
if (additionalSettings['apkFilterRegEx'] != null) {
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) => 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();
}

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

View File

@ -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) {

View File

@ -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,41 @@ class VLC extends AppSource {
VLC() {
host = 'videolan.org';
}
get dwUrlBase => 'https://get.$host/vlc-android/';
@override
Map<String, String>? get requestHeaders => HTML().requestHeaders;
@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)
@ -36,19 +60,32 @@ class VLC extends AppSource {
if (version == null) {
throw NoVersionError();
}
String? targetUrl = 'https://$dwUrlBase/$version/';
Response res2 = await sourceRequest(targetUrl);
String mirrorDwBase =
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
Response res2 = await get(Uri.parse(targetUrl));
List<String> apkUrls = [];
if (res2.statusCode == 200) {
apkUrls = parse(res2.body)
.querySelectorAll('a')
.map((e) => e.attributes['href'])
.map((e) => e.attributes['href']?.split('/').last)
.where((h) =>
h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk'))
.map((e) => mirrorDwBase + e!)
.map((e) => targetUrl + e!)
.toList();
} else if (res2.statusCode == 500 &&
res2.body.toLowerCase().indexOf('mirror') > 0) {
var html = parse(res2.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(res2);
}
@ -59,4 +96,20 @@ class VLC extends AppSource {
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 {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -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
: () {

View File

@ -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.19';
const String currentVersion = '0.14.2';
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,22 +154,28 @@ 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 {
AndroidAlarmManager.periodic(
Duration(minutes: existingUpdateInterval),
bgUpdateCheckAlarmId,
bgUpdateCheck,
rescheduleOnReboot: true,
wakeup: true);
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: actualUpdateInterval),
bgUpdateCheckAlarmId,
bgUpdateCheck,
rescheduleOnReboot: true,
wakeup: true);
}
}
existingUpdateInterval = actualUpdateInterval;
}
}
@ -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()));
});
}
}

View File

@ -16,7 +16,7 @@ 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'));
'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page'));
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) {

View File

@ -41,6 +41,7 @@ class _AddAppPageState extends State<AddAppPage> {
@override
Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
bool doingSomething = gettingAppInfo || searching;
@ -85,8 +86,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,11 +138,9 @@ 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;
@ -410,7 +408,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(() {
@ -504,6 +508,18 @@ class _AddAppPageState extends State<AddAppPage> {
HTML().runtimeType.toString()))
getHTMLSourceOverrideDropdown(),
if (shouldShowSearchBar()) getSearchBarRow(),
if (pickedSource != null)
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()),
const SizedBox(
height: 16,
),

View File

@ -153,10 +153,10 @@ class _AppPageState extends State<AppPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 125),
app?.installedInfo != null
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,
settingsProvider);
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(

View File

@ -381,7 +381,8 @@ class AppsPageState extends State<AppsPage> {
: () {
appsProvider.downloadAndInstallLatestApps(
[listedApps[appIndex].app.id],
globalNavigatorKey.currentContext).catchError((e) {
globalNavigatorKey.currentContext,
settingsProvider).catchError((e) {
showError(e, context);
return <String>[];
});
@ -393,9 +394,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(
@ -543,20 +544,19 @@ class AppsPageState extends State<AppsPage> {
: FontWeight.normal)),
trailing: listedApps[index].downloadProgress != null
? SizedBox(
width: 90,
child: Text(
listedApps[index].downloadProgress! >= 0
? tr('percentProgress', args: [
listedApps[index]
.downloadProgress!
.toInt()
.toString()
])
: tr('pleaseWait'),
textAlign: (listedApps[index].downloadProgress! >= 0)
? TextAlign.start
: TextAlign.end,
))
listedApps[index].downloadProgress! >= 0
? tr('percentProgress', args: [
listedApps[index]
.downloadProgress!
.toInt()
.toString()
])
: tr('pleaseWait'),
textAlign: (listedApps[index].downloadProgress! >= 0)
? TextAlign.start
: TextAlign.end,
))
: trailingRow,
onTap: () {
if (selectedAppIds.isNotEmpty) {
@ -644,15 +644,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,
@ -684,12 +684,15 @@ class AppsPageState extends State<AppsPage> {
toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(
toInstall, globalNavigatorKey.currentContext,
settingsProvider: settingsProvider)
.downloadAndInstallLatestApps(toInstall,
globalNavigatorKey.currentContext, settingsProvider)
.catchError((e) {
showError(e, context);
return <String>[];
}).then((value) {
if (shouldInstallUpdates) {
showError(tr('appsUpdated'), context);
}
});
}
});

View File

@ -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,10 +475,13 @@ class _SettingsPageState extends State<SettingsPage> {
Switch(
value:
settingsProvider.reversePageTransitions,
onChanged: (value) {
settingsProvider.reversePageTransitions =
value;
})
onChanged: settingsProvider
.disablePageTransitions
? null
: (value) {
settingsProvider
.reversePageTransitions = value;
})
],
),
height32,
@ -459,7 +534,44 @@ class _SettingsPageState extends State<SettingsPage> {
label: Text(tr('appLogs'))),
],
),
height16,
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'))
],
),
]),
),
],
),
)

View File

@ -5,21 +5,22 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
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';
@ -29,16 +30,19 @@ import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart';
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 +101,38 @@ 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;
}
moveStrToEndMapEntryWithCount(
List<MapEntry<String, int>> arr, MapEntry<String, int> str,
{MapEntry<String, int>? strB}) {
MapEntry<String, int>? temp;
arr.removeWhere((element) {
bool res = element.key == str.key || element.key == strB?.key;
if (res) {
temp = element;
}
return res;
});
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 = {};
@ -112,7 +148,7 @@ class AppsProvider with ChangeNotifier {
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 {
@ -130,20 +166,43 @@ class AppsProvider with ChangeNotifier {
APKDir.createSync();
}
}
// Load Apps into memory (in background, this is done later instead of in the constructor)
await loadApps();
// Delete any partial APKs
var cutoff = DateTime.now().subtract(const Duration(days: 7));
APKDir.listSync()
.where((element) =>
element.path.endsWith('.part') ||
element.statSync().modified.isBefore(cutoff))
.forEach((partialApk) {
partialApk.delete(recursive: true);
});
if (!isBg) {
// Load Apps into memory (in background processes, this is done later instead of in the constructor)
await loadApps();
// 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);
}
});
}
}();
}
Future<File> downloadFileWithRetry(
String url, String fileNameNoExt, Function? onProgress,
{bool useExisting = true,
Map<String, String>? headers,
int retries = 3}) async {
try {
return await downloadFile(url, fileNameNoExt, onProgress,
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 {
rethrow;
}
}
}
Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress,
{bool useExisting = true, Map<String, String>? headers}) async {
@ -196,19 +255,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) {
@ -219,9 +278,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;
@ -236,8 +294,9 @@ class AppsProvider with ChangeNotifier {
notificationsProvider?.cancel(notif.id);
int? prevProg;
var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
var downloadedFile = await downloadFile(downloadUrl, fileNameNoExt,
headers: source.requestHeaders, (double? progress) {
var downloadedFile = await downloadFileWithRetry(
downloadUrl, fileNameNoExt, headers: source.requestHeaders,
(double? progress) {
int? prog = progress?.ceil();
if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress;
@ -256,11 +315,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';
@ -270,10 +330,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;
@ -301,17 +362,41 @@ class AppsProvider with ChangeNotifier {
.where((element) => element.downloadProgress != null)
.isNotEmpty;
Future<bool> canInstallSilently(App app) async {
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;
Future<bool> canInstallSilently(
App app, SettingsProvider settingsProvider) async {
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 >= 30 &&
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 {
@ -325,40 +410,32 @@ 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(
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<void> installXApkDir(DownloadedXApkDir dir,
{bool silent = false}) async {
Future<void> installXApkDir(DownloadedXApkDir dir) 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>()) {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(DownloadedApk(dir.appId, file),
silent: silent);
await installApk(DownloadedApk(dir.appId, file));
} 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);
@ -366,25 +443,20 @@ 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) 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();
}
@ -478,9 +550,11 @@ class AppsProvider with ChangeNotifier {
// If no BuildContext is provided, apps that require user interaction are ignored
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
Future<List<String>> downloadAndInstallLatestApps(
List<String> appIds, BuildContext? context,
{SettingsProvider? settingsProvider}) async {
Future<List<String>> downloadAndInstallLatestApps(List<String> appIds,
BuildContext? context, SettingsProvider settingsProvider,
{NotificationsProvider? notificationsProvider}) async {
notificationsProvider =
notificationsProvider ?? context?.read<NotificationsProvider>();
List<String> appsToInstall = [];
List<String> trackOnlyAppsToUpdate = [];
// For all specified Apps, filter out those for which:
@ -506,7 +580,8 @@ class AppsProvider with ChangeNotifier {
apps[id]!.app.preferredApkIndex = urlInd;
await saveApps([apps[id]!.app]);
}
if (context != null || await canInstallSilently(apps[id]!.app)) {
if (context != null ||
await canInstallSilently(apps[id]!.app, settingsProvider)) {
appsToInstall.add(id);
}
}
@ -526,22 +601,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 {
// ignore: use_build_context_synchronously
var downloadedArtifact = await downloadApp(apps[id]!.app, context);
var downloadedArtifact =
// ignore: use_build_context_synchronously
await downloadApp(apps[id]!.app, context,
notificationsProvider: notificationsProvider);
DownloadedApk? downloadedFile;
DownloadedXApkDir? downloadedDir;
if (downloadedArtifact is DownloadedApk) {
@ -549,11 +617,10 @@ 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, settingsProvider);
if (!(await settingsProvider.getInstallPermission(enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
if (!willBeSilent && context != null) {
@ -564,9 +631,24 @@ class AppsProvider with ChangeNotifier {
notifyListeners();
try {
if (downloadedFile != null) {
await installApk(downloadedFile, silent: willBeSilent);
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installApk(downloadedFile);
} else {
await installApk(downloadedFile);
}
} else {
await installXApkDir(downloadedDir!, silent: willBeSilent);
if (willBeSilent && context == null) {
// Would await forever - workaround - TODO
installXApkDir(downloadedDir!);
} else {
await installXApkDir(downloadedDir!);
}
}
if (willBeSilent && context == null) {
notificationsProvider?.notify(SilentUpdateAttemptNotification(
[apps[appId]!.app],
id: appId.hashCode));
}
} finally {
apps[id]?.downloadProgress = null;
@ -582,8 +664,6 @@ class AppsProvider with ChangeNotifier {
throw errors;
}
NotificationsProvider().cancel(UpdateNotification([]).id);
return installedIds;
}
@ -596,27 +676,17 @@ class AppsProvider with ChangeNotifier {
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'] !=
@ -630,7 +700,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'] !=
@ -676,22 +747,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;
}
@ -730,7 +791,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));
}
@ -741,6 +802,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()));
@ -760,9 +825,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()]);
}
@ -775,34 +840,39 @@ 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)
apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id);
notifyListeners();
for (var app in apps.values) {
// 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
List<App> modifiedApps = [];
for (var app in apps.values) {
var moddedApp =
getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo);
if (moddedApp != null) {
modifiedApps.add(moddedApp);
}
// Reconcile version differences
List<App> modifiedApps = [];
for (var app in apps.values) {
var moddedApp =
getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo);
if (moddedApp != null) {
modifiedApps.add(moddedApp);
}
}
if (modifiedApps.isNotEmpty) {
await saveApps(modifiedApps, attemptToCorrectInstallStatus: false);
var removedAppIds = modifiedApps
.where((a) => a.installedVersion == null)
.map((e) => e.id)
.toList();
// After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds);
}
}
if (modifiedApps.isNotEmpty) {
await saveApps(modifiedApps, attemptToCorrectInstallStatus: false);
var removedAppIds = modifiedApps
.where((a) => a.installedVersion == null)
.map((e) => e.id)
.toList();
// After reconciliation, delete externally uninstalled Apps if needed
if (removedAppIds.isNotEmpty) {
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
if (settingsProvider.removeOnExternalUninstall) {
await removeApps(removedAppIds);
}
}
}
@ -814,12 +884,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;
}
@ -828,9 +898,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;
@ -872,6 +943,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
? []
@ -944,6 +1016,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 {
@ -952,17 +1040,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 {
@ -1179,3 +1258,213 @@ 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();
var settingsProvider = SettingsProvider();
await settingsProvider.initializeSettings();
int maxAttempts = 4;
params ??= {};
if (params['toCheck'] == null) {
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 (settingsProvider.bgUpdatesOnWiFiOnly) {
var netResult = await (Connectivity().checkConnectivity());
networkRestricted = (netResult != ConnectivityResult.wifi) &&
(netResult != ConnectivityResult.ethernet);
}
// Loop through all updates and check each
for (int i = 0; i < toCheck.length; i++) {
var appId = toCheck[i].key;
var retryCount = toCheck[i].value;
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, settingsProvider))) {
notificationsProvider.notify(
UpdateNotification([newApp], id: newApp.id.hashCode - 1));
} 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 (retryCount < maxAttempts) {
var remainingSeconds = e is RateLimitError
? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60))
: e is ClientException
? (15 * 60)
: (retryCount ^ 2);
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, retryCount + 1));
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);
}
}
}
}
// 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, settingsProvider,
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.');
}
}
}
}

View File

@ -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(
4,
tr('checkingForUpdates'),
'',
'BG_UPDATE_CHECK',
tr('checkingForUpdates'),
tr('checkingForUpdatesNotifDescription'),
Importance.min);
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 =

View File

@ -309,4 +309,43 @@ 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();
}
}

View File

@ -14,6 +14,7 @@ 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';
@ -28,6 +29,7 @@ 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;
@ -328,16 +330,23 @@ abstract class 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();
@ -354,10 +363,14 @@ abstract class AppSource {
Map<String, String>? get requestHeaders => null;
Future<Response> sourceRequest(String url) async {
if (requestHeaders != null) {
Future<Response> sourceRequest(String url,
{bool followRedirects = true}) async {
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 +425,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 +441,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;
@ -449,8 +485,11 @@ abstract class AppSource {
}
ObtainiumError getObtainiumHttpError(Response res) {
return ObtainiumError(res.reasonPhrase ??
tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
return ObtainiumError((res.reasonPhrase != null &&
res.reasonPhrase != null &&
res.reasonPhrase!.isNotEmpty)
? res.reasonPhrase!
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
}
abstract class MassAppUrlSource {
@ -485,10 +524,11 @@ class SourceProvider {
SourceHut(),
APKMirror(),
APKPure(),
HuaweiAppGallery(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(),
Signal(),
VLC(),
VLC(), // As of 2023-08-26 this site randomly messes up the 'latest' version (one minute it's 3.5.4, next minute back to 3.5.3)
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
TelegramApp(),
SteamMobile(),

View File

@ -5,27 +5,35 @@ 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:
@ -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:
@ -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:
@ -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: b1729fc96627dd44012d0a901558177418818d6bd428df59dcfeb594e5f66432
sha256: bdfa035a974a0c080576c4c8ed01cdf9d1b406a04c7daa05443ef0383a97bedc
url: "https://pub.dev"
source: hosted
version: "5.3.2"
version: "5.3.4"
flutter:
dependency: "direct main"
description: flutter
@ -267,10 +291,10 @@ packages:
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,10 +320,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "4b1bfbb802d76320a1a46d9ce984106135093efd9d969765d07c2125af107bdf"
sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431"
url: "https://pub.dev"
source: hosted
version: "0.6.17"
version: "0.6.17+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -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,50 +490,50 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0"
url: "https://pub.dev"
source: hosted
version: "2.0.15"
version: "2.1.0"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8"
url: "https://pub.dev"
source: hosted
version: "2.0.27"
version: "2.1.0"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.3.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3
url: "https://pub.dev"
source: hosted
version: "2.1.11"
version: "2.2.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84
url: "https://pub.dev"
source: hosted
version: "2.0.6"
version: "2.1.0"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da
url: "https://pub.dev"
source: hosted
version: "2.1.7"
version: "2.2.0"
permission_handler:
dependency: "direct main"
description:
@ -578,10 +586,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
plugin_platform_interface:
dependency: transitive
description:
@ -610,18 +618,18 @@ 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:
@ -642,10 +650,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_linux:
dependency: transitive
description:
@ -687,26 +695,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 +759,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:
@ -783,10 +791,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af"
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025"
url: "https://pub.dev"
source: hosted
version: "6.0.37"
version: "6.0.38"
url_launcher_ios:
dependency: transitive
description:
@ -851,46 +859,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: "04a0782fb058b7c71f2048935583488f4d32e9147ca403abc4e58f1de9964629"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.2.3"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "8587d0b4991bd0f223f4b4957101c2c7449f905601571315f4967072498dd3fb"
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6
url: "https://pub.dev"
source: hosted
version: "3.9.1"
version: "3.9.3"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30"
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.5.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "3e36a8f564809cb7c257ff4278502b185e2191349df0ddee98837f91805c74b8"
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972
url: "https://pub.dev"
source: hosted
version: "3.7.1"
version: "3.7.3"
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 +919,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
xml:
dependency: transitive
description:
@ -924,5 +940,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.0 <4.0.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.10.0"

View File

@ -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,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 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.19+183 # When changing this, update the tag in main() accordingly
version: 0.14.2+194 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=2.18.2 <3.0.0'
@ -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,7 @@ dependencies:
flutter_markdown: ^0.6.14
flutter_archive: ^5.0.0
hsluv: ^1.1.3
connectivity_plus: ^4.0.2
dev_dependencies:
flutter_test: