mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 04:53:28 +01:00 
			
		
		
		
	Compare commits
	
		
			182 Commits
		
	
	
		
			v0.13.23-b
			...
			v0.14.15-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 8d4a09b007 | ||
|  | 6d8d2fa99d | ||
|  | 59ecd55530 | ||
|  | 77fcc89e37 | ||
|  | 7d63da4d9c | ||
|  | 8eea0bae99 | ||
|  | f5dcd8f776 | ||
|  | e1f2baeeda | ||
|  | 84d8ff8b0b | ||
|  | 9862c22fdb | ||
|  | 8c82f53dd2 | ||
|  | 5cf4c21ce3 | ||
|  | 4951f62d4a | ||
|  | 4c0c4b7010 | ||
|  | 96051e614c | ||
|  | a2e494b2ba | ||
|  | 2d5a9bec84 | ||
|  | 7d9571cfdd | ||
|  | acc6a780fa | ||
|  | 0e36d42a06 | ||
|  | 5cfddd807a | ||
|  | 17b5604f2a | ||
|  | fdb6eed6d0 | ||
|  | 13de0437b8 | ||
|  | a43c45f310 | ||
|  | 9c56a4d1fc | ||
|  | 2aea1d2631 | ||
|  | 118e05a0fa | ||
|  | 05f497787e | ||
|  | 53cf4d0234 | ||
|  | 6e735b1763 | ||
|  | 873a1a0683 | ||
|  | 27b1149d1c | ||
|  | c1e64f111e | ||
|  | b2af8448fd | ||
|  | 8f44338e76 | ||
|  | e4a55abcb3 | ||
|  | d7348b4973 | ||
|  | 09421230f2 | ||
|  | 4596e32258 | ||
|  | 4dc007a4f6 | ||
|  | c53a156969 | ||
|  | 94bd0774fb | ||
|  | b178b1d780 | ||
|  | cbc840378c | ||
|  | aa7989c16d | ||
|  | 85f9336804 | ||
|  | d66be3ecda | ||
|  | c08e05bd6c | ||
|  | e08ab89fd4 | ||
|  | 8ba0a0a776 | ||
|  | 73ed0cea88 | ||
|  | 58a378d212 | ||
|  | 553307ba70 | ||
|  | 78f73a9049 | ||
|  | abc69e7a0e | ||
|  | 503914dbce | ||
|  | 69680f8680 | ||
|  | 1a616aacb7 | ||
|  | 52d19f267e | ||
|  | 75a8dc9ee4 | ||
|  | 8c63920ef6 | ||
|  | dda5619cdc | ||
|  | 9cc25fd931 | ||
|  | f5a9eb4295 | ||
|  | ebe256e482 | ||
|  | e215585a64 | ||
|  | 8b01fc03ec | ||
|  | 9af2c8370d | ||
|  | 01f9003b8d | ||
|  | 73a3c7eb71 | ||
|  | 370ec1432e | ||
|  | 4b63c124bb | ||
|  | 3d7df604b1 | ||
|  | 59d1d275bc | ||
|  | f7f0332b13 | ||
|  | 917c5f5083 | ||
|  | 370cf05615 | ||
|  | 101d892c7f | ||
|  | db2faf6511 | ||
|  | 3a83d1bf79 | ||
|  | 070e309aa6 | ||
|  | 11ca7c3f7d | ||
|  | 7a0acaefb3 | ||
|  | 486f3664ef | ||
|  | 387b165314 | ||
|  | 4ba3f7996e | ||
|  | 7ce41dfb67 | ||
|  | efd55c8d36 | ||
|  | 71543c64d4 | ||
|  | 408c1a541f | ||
|  | 8f739d9e0b | ||
|  | bd8f608ee6 | ||
|  | 551643b11c | ||
|  | 075ecae540 | ||
|  | dcb8807fa7 | ||
|  | 77ec2df31c | ||
|  | d92e812554 | ||
|  | 0a6e1f9cc6 | ||
|  | f3c3680382 | ||
|  | f7e783a556 | ||
|  | fd1b72563d | ||
|  | 5a94ef82dd | ||
|  | 3ad46b7e21 | ||
|  | 1e94d71665 | ||
|  | 0899a576ff | ||
|  | db2476f3a5 | ||
|  | 8ba182870d | ||
|  | d81085a9e8 | ||
|  | ae92a459b7 | ||
|  | e101c434d5 | ||
|  | 9686d0f0ca | ||
|  | 1dd0392b78 | ||
|  | 5e184d733b | ||
|  | ef0b20887b | ||
|  | 5cb4bd998b | ||
|  | 57f499c6a5 | ||
|  | c2ae6e19f9 | ||
|  | 16104fde03 | ||
|  | 13e10692b1 | ||
|  | 7fc93f23c0 | ||
|  | d6ddf87365 | ||
|  | 05172744cd | ||
|  | 2504ae24fc | ||
|  | 5e41d5762b | ||
|  | 57d44c972f | ||
|  | be0b57ac00 | ||
|  | 862bb2b276 | ||
|  | 5eac851f80 | ||
|  | 0deab8296f | ||
|  | 6785708661 | ||
|  | 5307fd0901 | ||
|  | 788c4c7917 | ||
|  | 3f6d96289d | ||
|  | 5cfd80e510 | ||
|  | 9eb32ae55a | ||
|  | 82e08150ab | ||
|  | e956ee9254 | ||
|  | bb4f34317b | ||
|  | d08ff3fbae | ||
|  | f5479ec82f | ||
|  | 3eb25c4060 | ||
|  | 03bb1ad9a6 | ||
|  | b59d3e77f9 | ||
|  | 7c41692d5f | ||
|  | ce89d456e1 | ||
|  | a0c48fcca6 | ||
|  | d0cba6d6bc | ||
|  | 6baf6ccf4b | ||
|  | a2571e61a8 | ||
|  | f61824ff0d | ||
|  | 734a1aeb01 | ||
|  | 5269aad90d | ||
|  | eaeee188eb | ||
|  | 8c850e06ca | ||
|  | 0f754a8da8 | ||
|  | 81c4d4f393 | ||
|  | 5317aee18d | ||
|  | b66eeba3b5 | ||
|  | 522ff1ddf7 | ||
|  | 7ef9c43ee3 | ||
|  | 05ac76e3e9 | ||
|  | 4838402797 | ||
|  | 0de12c7c07 | ||
|  | 0dadd8bffe | ||
|  | 75a0cb1189 | ||
|  | a52d936b4d | ||
|  | cfd04dc602 | ||
|  | 7622e63975 | ||
|  | 3e7172b9d1 | ||
|  | 7aa0294447 | ||
|  | 5b89c4d293 | ||
|  | 3bb991f57f | ||
|  | 1dd3aa0e8a | ||
|  | 189cecbc37 | ||
|  | 9680ba08e9 | ||
|  | dcf5bd37ca | ||
|  | d6a4b0a96d | ||
|  | 5f8638d349 | ||
|  | 1de274df39 | ||
|  | 3df801c54e | ||
|  | d473cb49c5 | 
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Something isn't working right. | ||||
| title: '' | ||||
| labels: bug, to check | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Prerequisites** | ||||
| <!-- Please ensure your request is not part of an existing issue. --> | ||||
|  | ||||
| **Describe the bug** | ||||
| <!-- A clear and concise description of what the bug is. --> | ||||
|  | ||||
| **To Reproduce** | ||||
| <!-- Steps to reproduce the behavior: | ||||
| 1. Go to '...' | ||||
| 2. Tap on '....' | ||||
| 3. Scroll down to '....' | ||||
| 4. See error --> | ||||
|  | ||||
| **Screenshots and Logs** | ||||
| <!-- If applicable, add screenshots, logs, and any other artifacts (like some/all files under `/Android/data/dev.imranr.obtainium/`) that you think may help troubleshoot the issue. --> | ||||
|  | ||||
| **Please complete the following information:** | ||||
|  - Device: <!-- [e.g. Pixel 7] --> | ||||
|  - OS: <!-- [e.g. GrapheneOS] --> | ||||
|  - Obtainium Version: <!-- [e.g. 0.14.6-beta] --> | ||||
|  | ||||
| **Additional context** | ||||
| <!-- Add any other context about the problem here. --> | ||||
							
								
								
									
										29
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| --- | ||||
| name: Feature request | ||||
| about: Suggest a new Source, setting, or other feature. | ||||
| title: '' | ||||
| labels: enhancement, to check | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Prerequisites** | ||||
| <!-- Please ensure your request is not part of an existing issue. --> | ||||
|  | ||||
| **Describe the feature** | ||||
| <!-- A clear and concise description of what you want to happen. | ||||
|  | ||||
| For new Sources, it's preferable (not required) if you suggest how the following details can be extracted from the Source in a reliable way (like an API or through web scraping): | ||||
| - The App version (or any release-specific identifier - a "pseudo-version") for the latest release | ||||
| - One or more APK URL(s) for the latest release | ||||
| - Above details for previous releases (optional) | ||||
|  | ||||
| Note that the Web scraper cannot deal with JavaScript-enabled content. --> | ||||
|  | ||||
| **Describe alternatives you've considered (if applicable)** | ||||
| <!-- A clear and concise description of any alternative solutions or features you've considered. | ||||
|  | ||||
| Note that app-specific Sources are less likely to be added. In those cases, see if the HTML Source will work for you (if not, see if a generally-applicable enhancement to the HTML Source would work, and suggest that instead). --> | ||||
|  | ||||
| **Additional context** | ||||
| <!-- Add any other context or screenshots about the feature request here. --> | ||||
							
								
								
									
										49
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,30 +2,36 @@ | ||||
|  | ||||
| Get Android App Updates Directly From the Source. | ||||
|  | ||||
| Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||
| Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||
|  | ||||
| Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | ||||
|  | ||||
| Currently supported App sources: | ||||
| - [GitHub](https://github.com/) | ||||
| - [GitLab](https://gitlab.com/) | ||||
| - [Codeberg](https://codeberg.org/) | ||||
| - [F-Droid](https://f-droid.org/) | ||||
| - [IzzyOnDroid](https://android.izzysoft.de/) | ||||
| - [Mullvad](https://mullvad.net/en/) | ||||
| - [Signal](https://signal.org/) | ||||
| - [SourceForge](https://sourceforge.net/) | ||||
| - [SourceHut](https://git.sr.ht/) | ||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||
| - [APKPure](https://apkpure.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) | ||||
| - Open Source - General: | ||||
|   - [GitHub](https://github.com/) | ||||
|   - [GitLab](https://gitlab.com/) | ||||
|   - [Codeberg](https://codeberg.org/) | ||||
|   - [F-Droid](https://f-droid.org/) | ||||
|   - Third Party F-Droid Repos | ||||
|   - [IzzyOnDroid](https://android.izzysoft.de/) | ||||
|   - [SourceForge](https://sourceforge.net/) | ||||
|   - [SourceHut](https://git.sr.ht/) | ||||
| - Other - General: | ||||
|   - [APKPure](https://apkpure.com/) | ||||
|   - [Aptoide](https://aptoide.com/) | ||||
|   - [Uptodown](https://uptodown.com/) | ||||
|   - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||
|   - [Huawei AppGallery](https://appgallery.huawei.com/) | ||||
|   - Jenkins Jobs | ||||
| - Open Source - App-Specific: | ||||
|   - [Mullvad](https://mullvad.net/en/) | ||||
|   - [Signal](https://signal.org/) | ||||
|   - [VLC](https://videolan.org/) | ||||
| - Other - App-Specific: | ||||
|   - [Telegram App](https://telegram.org) | ||||
|   - [Steam Mobile Apps](https://store.steampowered.com/mobile) | ||||
|   - [Neutron Code](https://neutroncode.com) | ||||
| - "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| @@ -33,8 +39,9 @@ Currently supported App sources: | ||||
|     alt="Get it on GitHub" | ||||
|     height="80">](https://github.com/ImranR98/Obtainium/releases) | ||||
|  | ||||
| [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) | ||||
|  | ||||
| ## Limitations | ||||
| - Auto (unattended) updates are unsupported due to a lack of any capable Flutter plugin. | ||||
| - For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable. | ||||
|  | ||||
| ## Screenshots | ||||
|   | ||||
| @@ -49,7 +49,6 @@ android { | ||||
|     } | ||||
|  | ||||
|     defaultConfig { | ||||
|         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | ||||
|         applicationId "dev.imranr.obtainium" | ||||
|         // You can update the following values to match your application needs. | ||||
|         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. | ||||
|   | ||||
| @@ -62,6 +62,7 @@ | ||||
|     </application> | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | ||||
|     <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> | ||||
|     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||
| @@ -70,4 +71,5 @@ | ||||
|     <uses-permission | ||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||
|         android:maxSdkVersion="29"/> | ||||
|     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> | ||||
| </manifest> | ||||
							
								
								
									
										313
									
								
								assets/translations/br.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								assets/translations/br.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | ||||
| { | ||||
|     "invalidURLForSource": "URL {} inválida", | ||||
|     "noReleaseFound": "Não foi possivel encontrar uma versão adequada", | ||||
|     "noVersionFound": "Não foi possivel encontrar uma versão lançada", | ||||
|     "urlMatchesNoSource": "URL não corresponde a uma fonte conhecida", | ||||
|     "cantInstallOlderVersion": "Não pode instalar uma versão anterior de um App", | ||||
|     "appIdMismatch": "ID do pacote baixado não é igual ao ID do App instalado", | ||||
|     "functionNotImplemented": "Esta classe não implementou essa função", | ||||
|     "placeholder": "Espaço Reservado", | ||||
|     "someErrors": "Alguns Erros Ocorreram", | ||||
|     "unexpectedError": "Erro Inesperado", | ||||
|     "ok": "Ok", | ||||
|     "and": "e", | ||||
|     "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", | ||||
|     "includePrereleases": "Incluir pré-lançamentos", | ||||
|     "fallbackToOlderReleases": "Retornar para versões anteriores", | ||||
|     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", | ||||
|     "invalidRegEx": "Expressão Regular Inválida", | ||||
|     "noDescription": "Sem descrição", | ||||
|     "cancel": "Cancelar", | ||||
|     "continue": "Continuar", | ||||
|     "requiredInBrackets": "(Necessário)", | ||||
|     "dropdownNoOptsError": "ERRO: O DROPDOWN DEVE TER PELO MENOS UMA OPÇÃO", | ||||
|     "colour": "Cor", | ||||
|     "githubStarredRepos": "Favoritados no GitHub", | ||||
|     "uname": "Nome de usuário", | ||||
|     "wrongArgNum": "Número de argumentos errado", | ||||
|     "xIsTrackOnly": "{} é 'Apenas Seguir'", | ||||
|     "source": "Fonte", | ||||
|     "app": "App", | ||||
|     "appsFromSourceAreTrackOnly": "Os apps desta fonte são 'Apenas Seguir'.", | ||||
|     "youPickedTrackOnly": "Você selecionou a opção 'Apenas Seguir'.", | ||||
|     "trackOnlyAppDescription": "Esse App vai ser seguido por atualizações, mais o Obtainium não poderá baixa-lo ou instala-lo.", | ||||
|     "cancelled": "Cancelado", | ||||
|     "appAlreadyAdded": "App já adicionado", | ||||
|     "alreadyUpToDateQuestion": "App já atualizado?", | ||||
|     "addApp": "Adicionar App", | ||||
|     "appSourceURL": "URL de origem do App", | ||||
|     "error": "Erro", | ||||
|     "add": "Adicionar", | ||||
|     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", | ||||
|     "search": "Procurar", | ||||
|     "additionalOptsFor": "Opções Adicionais para {}", | ||||
|     "supportedSources": "Fontes Compatíveis", | ||||
|     "trackOnlyInBrackets": "(Apenas Seguir)", | ||||
|     "searchableInBrackets": "(Pesquisável)", | ||||
|     "appsString": "Apps", | ||||
|     "noApps": "Sem Apps", | ||||
|     "noAppsForFilter": "Sem Apps para Filtrar", | ||||
|     "byX": "Por {}", | ||||
|     "percentProgress": "Progresso: {}%", | ||||
|     "pleaseWait": "Por Favor Espere", | ||||
|     "updateAvailable": "Atualização Disponível", | ||||
|     "estimateInBracketsShort": "(Aprox.)", | ||||
|     "notInstalled": "Não Instalado", | ||||
|     "estimateInBrackets": "(Aproximado)", | ||||
|     "selectAll": "Selecionar All", | ||||
|     "deselectN": "Deselecionar {}", | ||||
|     "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.", | ||||
|     "removeSelectedAppsQuestion": "Remover Apps Selecionados?", | ||||
|     "removeSelectedApps": "Remover Apps Selecionados", | ||||
|     "updateX": "Atualizar {}", | ||||
|     "installX": "Instalar {}", | ||||
|     "markXTrackOnlyAsUpdated": "Marcar {}\n(Apenas Seguir)\ncomo Atualizado", | ||||
|     "changeX": "Mudar {}", | ||||
|     "installUpdateApps": "Instalar/Atualizar Apps", | ||||
|     "installUpdateSelectedApps": "Instalar/Atualizar Apps Selecionados", | ||||
|     "markXSelectedAppsAsUpdated": "Marcar {} Apps Delecionados como Atualizados?", | ||||
|     "no": "Não", | ||||
|     "yes": "Sim", | ||||
|     "markSelectedAppsUpdated": "Marcar Apps Selecionados como Atualizados", | ||||
|     "pinToTop": "Fixar no topo", | ||||
|     "unpinFromTop": "Desafixar do topo", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "Reiniciar Status de Instalação para Apps Seleciondos?", | ||||
|     "installStatusOfXWillBeResetExplanation": "O status de instalação de qualquer app selecionado sera reiniciado.\n\nIsso pode ajudar quando uma versão de um App mostrada no Obtainium é incorreta devido a falhas ao atualizar ou outros problemas.", | ||||
|     "shareSelectedAppURLs": "Compartilhar URLs de Apps Selecionados", | ||||
|     "resetInstallStatus": "Reiniciar Status de Instalação", | ||||
|     "more": "Mais", | ||||
|     "removeOutdatedFilter": "Remover Filtro de Apps Desatualizados", | ||||
|     "showOutdatedOnly": "Mostrar Apenas Apps Desatualizados", | ||||
|     "filter": "Filtro", | ||||
|     "filterActive": "Filtro *", | ||||
|     "filterApps": "Filtrar Apps", | ||||
|     "appName": "Nome do App", | ||||
|     "author": "Autor", | ||||
|     "upToDateApps": "Apps Atualizados", | ||||
|     "nonInstalledApps": "Apps Não Instalados", | ||||
|     "importExport": "Importar/Exportar", | ||||
|     "settings": "Configurações", | ||||
|     "exportedTo": "Exportado para {}", | ||||
|     "obtainiumExport": "Exportar Obtainium", | ||||
|     "invalidInput": "Input Inválido", | ||||
|     "importedX": "Importado {}", | ||||
|     "obtainiumImport": "Importar Obtainium", | ||||
|     "importFromURLList": "Importar de Lista de URLs", | ||||
|     "searchQuery": "Pesquisa", | ||||
|     "appURLList": "Lista de URLs de Apps", | ||||
|     "line": "Linha", | ||||
|     "searchX": "Pesquisa {}", | ||||
|     "noResults": "Nenhum resultado encontrado", | ||||
|     "importX": "Importar {}", | ||||
|     "importedAppsIdDisclaimer": "Apps Importados podem ser mostrados incorretamente como \"Não Instalado\".\nPara consertar, reinstale-os usando o Obtainium.\nIsso não deve afetar dados do App.\n\nAfeta apenas métodos de importação de URL e de terceiros.", | ||||
|     "importErrors": "Erros de Importação", | ||||
|     "importedXOfYApps": "{} de {} Apps importados.", | ||||
|     "followingURLsHadErrors": "As seguintes URLs apresentaram erros:", | ||||
|     "okay": "Ok", | ||||
|     "selectURL": "Selecionar URL", | ||||
|     "selectURLs": "Selecionar URLs", | ||||
|     "pick": "Escolher", | ||||
|     "theme": "Tema", | ||||
|     "dark": "Escuro", | ||||
|     "light": "Claro", | ||||
|     "followSystem": "Seguir o Sistema", | ||||
|     "obtainium": "Obtainium", | ||||
|     "materialYou": "Material You", | ||||
|     "useBlackTheme": "Usar tema preto completamente escuro", | ||||
|     "appSortBy": "Classificar App por", | ||||
|     "authorName": "Autor/Nome", | ||||
|     "nameAuthor": "Nome/Autor", | ||||
|     "asAdded": "Como Adicionado", | ||||
|     "appSortOrder": "Ordem de classificação de Apps", | ||||
|     "ascending": "Ascendente", | ||||
|     "descending": "Descendente", | ||||
|     "bgUpdateCheckInterval": "Intervalo de verificação de atualizações em segundo plano", | ||||
|     "neverManualOnly": "Nunca - Apenas Manual", | ||||
|     "appearance": "Aparência", | ||||
|     "showWebInAppView": "Mostrar páginas da internet em App view", | ||||
|     "pinUpdates": "Fixar atualizações no topo da visão de Apps", | ||||
|     "updates": "Atualizações", | ||||
|     "sourceSpecific": "Específico a fonte", | ||||
|     "appSource": "Fonte de Apps", | ||||
|     "noLogs": "Sem Logs", | ||||
|     "appLogs": "Logs do App", | ||||
|     "close": "Fechar", | ||||
|     "share": "Compartilhar", | ||||
|     "appNotFound": "App não encontrado", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||
|     "pickAnAPK": "Selecionar um APK", | ||||
|     "appHasMoreThanOnePackage": "{} tem mais de um pacote:", | ||||
|     "deviceSupportsXArch": "Seu dispositivo suporta a arquitetura de CPU {}.", | ||||
|     "deviceSupportsFollowingArchs": "Seu dispositivo suporta as seguintes arquiteturas de CPU:", | ||||
|     "warning": "Aviso", | ||||
|     "sourceIsXButPackageFromYPrompt": "A Fonte do App é '{}' mais o pacote lançado vem de '{}'. Continuar?", | ||||
|     "updatesAvailable": "Atualizações Disponíveis", | ||||
|     "updatesAvailableNotifDescription": "Notifica o usuário quando atualizações estão disponíveis um ou mais Apps seguidos pelo Obtainium", | ||||
|     "noNewUpdates": "Sem novas atualizações.", | ||||
|     "xHasAnUpdate": "{} tem uma atualização.", | ||||
|     "appsUpdated": "Apps Atualizados", | ||||
|     "appsUpdatedNotifDescription": "Notifica o usuário quando atualizações para um ou mais Apps foram aplicadas em segundo plano", | ||||
|     "xWasUpdatedToY": "{} foi atualizado para {}.", | ||||
|     "errorCheckingUpdates": "Erro ao Procurar por Atualizações", | ||||
|     "errorCheckingUpdatesNotifDescription": "Uma notificação que mostra quando a checagem por atualizações em segundo plano falha", | ||||
|     "appsRemoved": "Apps Removidos", | ||||
|     "appsRemovedNotifDescription": "Notifica o usuário quando um ou mais Apps foram removidos devido a erros ao carregá-los", | ||||
|     "xWasRemovedDueToErrorY": "{} foi removido devido a este erro: {}", | ||||
|     "completeAppInstallation": "Instalação completa do App", | ||||
|     "obtainiumMustBeOpenToInstallApps": "Obtainium deve estar aberto para instalar Apps", | ||||
|     "completeAppInstallationNotifDescription": "Pede ao usuário que retorne ao Obtainium para finalizar a instalação de um App", | ||||
|     "checkingForUpdates": "Checando por Atualizações", | ||||
|     "checkingForUpdatesNotifDescription": "Notificação transiente que aparece quando checando por atualizações", | ||||
|     "pleaseAllowInstallPerm": "Por favor, permita o Obtainium instalar Apps", | ||||
|     "trackOnly": "Apenas Seguir", | ||||
|     "errorWithHttpStatusCode": "Erro {}", | ||||
|     "versionCorrectionDisabled": "Correção de versão desativada (plugin parece não funcionar)", | ||||
|     "unknown": "Desconhecido", | ||||
|     "none": "Nenhum", | ||||
|     "never": "Nunca", | ||||
|     "latestVersionX": "Última versão: {}", | ||||
|     "installedVersionX": "Versão Instalada: {}", | ||||
|     "lastUpdateCheckX": "Última Checagem por Atualização: {}", | ||||
|     "remove": "Remover", | ||||
|     "yesMarkUpdated": "Sim, Marcar como Atualizado", | ||||
|     "fdroid": "F-Droid Official", | ||||
|     "appIdOrName": "ID do App ou Nome", | ||||
|     "appId": "ID do App", | ||||
|     "appWithIdOrNameNotFound": "Nenhum App foi encontrado com esse ID ou nome", | ||||
|     "reposHaveMultipleApps": "Repositórios podem conter multiplos Apps", | ||||
|     "fdroidThirdPartyRepo": "Repositórios de terceiros F-Droid", | ||||
|     "steam": "Steam", | ||||
|     "steamMobile": "Steam Mobile", | ||||
|     "steamChat": "Steam Chat", | ||||
|     "install": "Instalar", | ||||
|     "markInstalled": "Marcar Instalado", | ||||
|     "update": "Atualizar", | ||||
|     "markUpdated": "Marcar Atualizado", | ||||
|     "additionalOptions": "Opções Adicionais", | ||||
|     "disableVersionDetection": "Desativar Detecção de Versão", | ||||
|     "noVersionDetectionExplanation": "Essa opção deve apenas ser usada por Apps onde detecção de versão não funciona corretamente.", | ||||
|     "downloadingX": "Baixando {}", | ||||
|     "downloadNotifDescription": "Notifica o usuário do progresso ao baixar um App", | ||||
|     "noAPKFound": "APK não encontrado", | ||||
|     "noVersionDetection": "Sem Detecção de versão", | ||||
|     "categorize": "Categorizar", | ||||
|     "categories": "Categorias", | ||||
|     "category": "Categoria", | ||||
|     "noCategory": "Sem Categoria", | ||||
|     "noCategories": "Sem Categoria", | ||||
|     "deleteCategoriesQuestion": "Deletar Categorias?", | ||||
|     "categoryDeleteWarning": "Todos os Apps em categorias removidas serão descategorizados.", | ||||
|     "addCategory": "Adicionar Categoria", | ||||
|     "label": "Etiqueta", | ||||
|     "language": "Linguagem", | ||||
|     "copiedToClipboard": "Copiado para a área de transferência", | ||||
|     "storagePermissionDenied": "Permição ao armazenamento negada", | ||||
|     "selectedCategorizeWarning": "Isso vai substituir qualquer confirução de categoria para os Apps selecionados.", | ||||
|     "filterAPKsByRegEx": "Filtrar APKs por Expressão Regular", | ||||
|     "removeFromObtainium": "Remover do Obtainium", | ||||
|     "uninstallFromDevice": "Desinstalar do dispositivo", | ||||
|     "onlyWorksWithNonVersionDetectApps": "Apenas funciona para Apps com detecção de versão desativada.", | ||||
|     "releaseDateAsVersion": "Usar Data de Lançamento como Versão", | ||||
|     "releaseDateAsVersionExplanation": "Esta opção só deve ser usada para aplicativos onde a detecção de versão não funciona corretamente, mas há uma data de lançamento disponível.", | ||||
|     "changes": "Mudanças", | ||||
|     "releaseDate": "Data de Lançamento", | ||||
|     "importFromURLsInFile": "Importar de URLs em Arquivo (como OPML)", | ||||
|     "versionDetection": "Detecção de Versão", | ||||
|     "standardVersionDetection": "Detecção de versão padrão", | ||||
|     "groupByCategory": "Agroupar por Categoria", | ||||
|     "autoApkFilterByArch": "Tente filtrar APKs por arquitetura de CPU, se possível", | ||||
|     "overrideSource": "Substituir Fonte", | ||||
|     "dontShowAgain": "Não mostrar isso novamente", | ||||
|     "dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Seguir'", | ||||
|     "dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK", | ||||
|     "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps", | ||||
|     "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)", | ||||
|     "about": "Sobre", | ||||
|     "requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)", | ||||
|     "checkOnStart": "Checar por atualizações ao iniciar ", | ||||
|     "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte", | ||||
|     "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente", | ||||
|     "pickHighestVersionCode": "Auto-selecionar o maior numero de versão do APK", | ||||
|     "checkUpdateOnDetailPage": "Checar por atualizações ao abrir a pagina de detalhes de um App", | ||||
|     "disablePageTransitions": "Desativar animações de transição de pagina", | ||||
|     "reversePageTransitions": "Reverter animações de transição de pagina", | ||||
|     "minStarCount": "Contagem Minima de Estrelas", | ||||
|     "addInfoBelow": "Adicionar essa informação abaixo.", | ||||
|     "addInfoInSettings": "Adicionar essa informação nas configurações.", | ||||
|     "githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.", | ||||
|     "gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.", | ||||
|     "sortByFileNamesNotLinks": "Classifique por nomes de arquivos em vez de links completos", | ||||
|     "filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular", | ||||
|     "customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')", | ||||
|     "appsPossiblyUpdated": "Tentativas de atualização de Apps", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifica o usuário de que atualizações de um ou mais Apps foram potencialmente aplicadas em segundo plano", | ||||
|     "xWasPossiblyUpdatedToY": "{} pode ter sido atualizado para {}.", | ||||
|     "enableBackgroundUpdates": "Ativar atualizações em segundo plano", | ||||
|     "backgroundUpdateReqsExplanation": "Atualizações em segundo plano podem não ser possíveis para todos os Apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.", | ||||
|     "verifyLatestTag": "Verifique a 'ultima' etiqueta", | ||||
|     "intermediateLinkRegex": "Filtre por um Link 'Intermediário' para Visitar Primeiro", | ||||
|     "intermediateLinkNotFound": "Link intermediário não encontrado", | ||||
|     "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", | ||||
|     "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remover App?", | ||||
|         "other": "Remover Apps?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Muitas solicitações (taxa limitada) - tente novamente em {} minuto", | ||||
|         "other": "Muitas solicitações (taxa limitada) - tente novamente em {} minutos" | ||||
|     }, | ||||
|     "bgUpdateGotErrorRetryInMinutes": { | ||||
|         "one": "A verificação de atualizações em segundo plano encontrou um {}, agendada uma nova verificação em {} minuto", | ||||
|         "other": "A verificação de atualizações em segundo plano encontrou um {}, agendada uma nova verificação em {} minutos" | ||||
|     }, | ||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||
|         "one": "A verificação de atualizações em segundo plano encontrou {} atualização, o usuário sera notificado caso necessário", | ||||
|         "other": "A verificação de atualizações em segundo plano encontrou {} atualizações, o usuário sera notificado caso necessário" | ||||
|     }, | ||||
|     "apps": { | ||||
|         "one": "{} App", | ||||
|         "other": "{} Apps" | ||||
|     }, | ||||
|     "url": { | ||||
|         "one": "{} URL", | ||||
|         "other": "{} URLs" | ||||
|     }, | ||||
|     "minute": { | ||||
|         "one": "{} Minuto", | ||||
|         "other": "{} Minutos" | ||||
|     }, | ||||
|     "hour": { | ||||
|         "one": "{} Hora", | ||||
|         "other": "{} Horas" | ||||
|     }, | ||||
|     "day": { | ||||
|         "one": "{} Dia", | ||||
|         "other": "{} Dias" | ||||
|     }, | ||||
|     "clearedNLogsBeforeXAfterY": { | ||||
|         "one": "Limpo {n} log (antes = {antes}, depois = {depois})", | ||||
|         "other": "Limpados {n} logs (antes = {antes}, depois = {depois})" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "{} e 1 outro app tem atualizações.", | ||||
|         "other": "{} e {} outros apps tem atualizações." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} e 1 outro app foi atualizado.", | ||||
|         "other": "{} e {} outros apps foram atualizados." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} e 1 outro app pode ter sido atualizado.", | ||||
|         "other": "{} e {} outros apps podem ter sido atualizados." | ||||
|     } | ||||
| } | ||||
| @@ -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,15 +11,7 @@ | ||||
|    "unexpectedError": "Neočekivana greška", | ||||
|    "ok": "Dobro", | ||||
|    "and": "i", | ||||
|    "startedBgUpdateTask": "Započeo je pozadinski zadatak provjere ažuriranja", | ||||
|    "bgUpdateIgnoreAfterIs": "ignoreAfter pozadinskog zadataka je  {}", | ||||
|    "startedActualBGUpdateCheck": "Započela je stvarna provjera ažuriranja", | ||||
|    "bgUpdateTaskFinished": "Završen zadatak provjere ažuriranja", | ||||
|    "firstRun": "Ovo je prvi put da pokrećete Obtainium", | ||||
|    "settingUpdateCheckIntervalTo": "Podešavanje intervala ažuriranja na {}", | ||||
|    "githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)", | ||||
|    "githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token", | ||||
|    "githubPATFormat": "korisničko_ime:token", | ||||
|    "includePrereleases": "Uključi preliminarna izdanja", | ||||
|    "fallbackToOlderReleases": "Povratak na starija izdanja", | ||||
|    "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", | ||||
| @@ -49,7 +41,7 @@ | ||||
|    "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", | ||||
|    "search": "Pretraživanje", | ||||
|    "additionalOptsFor": "Dodatne opcije za {}", | ||||
|    "supportedSourcesBelow": "Podržani izvori:", | ||||
|    "supportedSources": "Podržani izvori", | ||||
|    "trackOnlyInBrackets": "(Samo za praćenje)", | ||||
|    "searchableInBrackets": "(Može se pretraživati)", | ||||
|    "appsString": "Aplikacije", | ||||
| @@ -119,7 +111,7 @@ | ||||
|    "dark": "Tamna", | ||||
|    "light": "Svijetla", | ||||
|    "followSystem": "Pratite sistem", | ||||
|    "obtainium": "Obtainium", | ||||
|     "obtainium": "Obtainium", | ||||
|    "materialYou": "Material You", | ||||
|    "useBlackTheme": "Koristite čisto crnu tamnu temu", | ||||
|    "appSortBy": "Aplikacije sortirane po", | ||||
| @@ -246,7 +238,24 @@ | ||||
|    "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", | ||||
|    "removeAppQuestion": { | ||||
|    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|    "appsPossiblyUpdated": "App Updates Attempted", | ||||
|    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|    "verifyLatestTag": "Verify the 'latest' tag", | ||||
|    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|    "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|    "matchGroupToUse": "Match Group to Use", | ||||
|    "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|    "pickExportDir": "Pick Export Directory", | ||||
|    "autoExportOnChanges": "Auto-export on changes", | ||||
|    "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|      "removeAppQuestion": { | ||||
|       "one": "Želite li ukloniti aplikaciju?", | ||||
|       "other": "Želite li ukloniti aplikacije?" | ||||
|    }, | ||||
| @@ -293,5 +302,9 @@ | ||||
|    "xAndNMoreUpdatesInstalled": { | ||||
|       "one": "{} i još 1 aplikacija je ažurirana.", | ||||
|       "other": "{} i još {} aplikacija je ažurirano." | ||||
|    }, | ||||
|    "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|       "one": "{} and 1 more app may have been updated.", | ||||
|       "other": "{} and {} more apps may have been updated." | ||||
|    } | ||||
| } | ||||
| } | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Unerwarteter Fehler", | ||||
|     "ok": "Okay", | ||||
|     "and": "und", | ||||
|     "startedBgUpdateTask": "Hintergrundaktualisierungsprüfung gestartet", | ||||
|     "bgUpdateIgnoreAfterIs": "Hintergrundaktualisierung 'ignoreAfter' ist {}", | ||||
|     "startedActualBGUpdateCheck": "Überprüfung der Hintergrundaktualisierung gestartet", | ||||
|     "bgUpdateTaskFinished": "Hintergrundaktualisierungsprüfung abgeschlossen", | ||||
|     "firstRun": "Dies ist der erste Start von Obtainium überhaupt", | ||||
|     "settingUpdateCheckIntervalTo": "Aktualisierungsintervall auf {} stellen", | ||||
|     "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", | ||||
|     "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token", | ||||
|     "githubPATFormat": "Benutzername:Token", | ||||
|     "includePrereleases": "Vorabversionen einbeziehen", | ||||
|     "fallbackToOlderReleases": "Fallback auf ältere Versionen", | ||||
|     "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", | ||||
| @@ -36,8 +28,8 @@ | ||||
|     "xIsTrackOnly": "{} ist nur zur Nachverfolgung", | ||||
|     "source": "Quelle", | ||||
|     "app": "App", | ||||
|     "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.", | ||||
|     "youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.", | ||||
|     "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.", | ||||
|     "youPickedTrackOnly": "Sie haben die Option „Nur Nachverfolgen“ gewählt.", | ||||
|     "trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.", | ||||
|     "cancelled": "Abgebrochen", | ||||
|     "appAlreadyAdded": "App bereits hinzugefügt", | ||||
| @@ -46,10 +38,10 @@ | ||||
|     "appSourceURL": "Quell-URL der App", | ||||
|     "error": "Fehler", | ||||
|     "add": "Hinzufügen", | ||||
|     "searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)", | ||||
|     "searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)", | ||||
|     "search": "Suchen", | ||||
|     "additionalOptsFor": "Zusatzoptionen für {}", | ||||
|     "supportedSourcesBelow": "Unterstützte Quellen:", | ||||
|     "supportedSources": "Unterstützte Quellen", | ||||
|     "trackOnlyInBrackets": "(Nur Nachverfolgen)", | ||||
|     "searchableInBrackets": "(Durchsuchbar)", | ||||
|     "appsString": "Apps", | ||||
| @@ -78,7 +70,7 @@ | ||||
|     "yes": "Ja", | ||||
|     "markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell", | ||||
|     "pinToTop": "Oben anheften", | ||||
|     "unpinFromTop": "'Oben anheften' aufheben", | ||||
|     "unpinFromTop": "„Oben anheften“ aufheben", | ||||
|     "resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?", | ||||
|     "installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.", | ||||
|     "shareSelectedAppURLs": "Ausgewählte App-URLs teilen", | ||||
| @@ -183,7 +175,7 @@ | ||||
|     "appId": "App ID", | ||||
|     "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", | ||||
|     "reposHaveMultipleApps": "Repos können mehrere Apps enthalten", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Third-Party Repo", | ||||
|     "fdroidThirdPartyRepo": "F-Droid Drittparteienrepo", | ||||
|     "steam": "Steam", | ||||
|     "steamMobile": "Steam Mobile", | ||||
|     "steamChat": "Steam Chat", | ||||
| @@ -229,7 +221,7 @@ | ||||
|     "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", | ||||
| @@ -244,8 +236,25 @@ | ||||
|     "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": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links", | ||||
|     "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern", | ||||
|     "customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Aktualisierungen wurden versucht", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Benachrichtigt den Benutzer, dass Updates für eine oder mehrere Apps möglicherweise im Hintergrund durchgeführt wurden", | ||||
|     "xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.", | ||||
|     "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.", | ||||
|     "verifyLatestTag": "Überprüfe das „latest“ Tag", | ||||
|     "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", | ||||
|     "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", | ||||
|     "autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen", | ||||
|     "versionExtractionRegEx": "Versions-Extraktion RegEx", | ||||
|     "matchGroupToUse": "Zu verwendende Gruppe abgleichen", | ||||
|     "highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben", | ||||
|     "pickExportDir": "Export-Verzeichnis wählen", | ||||
|     "autoExportOnChanges": "Automatischer Export bei Änderung", | ||||
|     "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern", | ||||
|     "trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "App entfernen?", | ||||
|         "other": "Apps entfernen?" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} und 1 weitere Anwendung wurden aktualisiert.", | ||||
|         "other": "{} und {} weitere Anwendungen wurden aktualisiert." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} und 1 weitere Anwendung wurden möglicherweise aktualisiert.", | ||||
|         "other": "{} und {} weitere Anwendungen wurden möglicherweise aktualisiert." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Unexpected Error", | ||||
|     "ok": "Okay", | ||||
|     "and": "and", | ||||
|     "startedBgUpdateTask": "Started BG update check task", | ||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is  {}", | ||||
|     "startedActualBGUpdateCheck": "Started actual BG update checking", | ||||
|     "bgUpdateTaskFinished": "Finished BG update check task", | ||||
|     "firstRun": "This is the first ever run of Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Setting update interval to {}", | ||||
|     "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", | ||||
|     "githubPATHint": "PAT must be in this format: username:token", | ||||
|     "githubPATFormat": "username:token", | ||||
|     "includePrereleases": "Include prereleases", | ||||
|     "fallbackToOlderReleases": "Fallback to older releases", | ||||
|     "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Search (Some Sources Only)", | ||||
|     "search": "Search", | ||||
|     "additionalOptsFor": "Additional Options for {}", | ||||
|     "supportedSourcesBelow": "Supported Sources:", | ||||
|     "supportedSources": "Supported Sources", | ||||
|     "trackOnlyInBrackets": "(Track-Only)", | ||||
|     "searchableInBrackets": "(Searchable)", | ||||
|     "appsString": "Apps", | ||||
| @@ -246,6 +238,26 @@ | ||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "enableBackgroundUpdates": "Enable background updates", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First", | ||||
|     "intermediateLinkNotFound": "Intermediate link not found", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Remove App?", | ||||
|         "other": "Remove Apps?" | ||||
| @@ -291,7 +303,11 @@ | ||||
|         "other": "{} and {} more apps have updates." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} and 1 more app were updated.", | ||||
|         "one": "{} and 1 more app was updated.", | ||||
|         "other": "{} and {} more apps were updated." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Error Inesperado", | ||||
|     "ok": "Correcto", | ||||
|     "and": "y", | ||||
|     "startedBgUpdateTask": "Empezada la tarea de comprobación de actualizaciones en segundo plano", | ||||
|     "bgUpdateIgnoreAfterIs": "El parámetro ignoreAfter de la actualización en segundo plano es  {}", | ||||
|     "startedActualBGUpdateCheck": "Ha comenzado la comprobación de actualizaciones en segundo plano", | ||||
|     "bgUpdateTaskFinished": "Ha finalizado la comprobación de actualizaciones en segundo plano", | ||||
|     "firstRun": "Esta es la primera ejecución de Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Cambiando intervalo de actualización a {}", | ||||
|     "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", | ||||
|     "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token", | ||||
|     "githubPATFormat": "nombre_de_usuario:token", | ||||
|     "includePrereleases": "Incluir versiones preliminares", | ||||
|     "fallbackToOlderReleases": "Retorceder a versiones previas", | ||||
|     "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", | ||||
|     "search": "Buscar", | ||||
|     "additionalOptsFor": "Opciones Adicionales para {}", | ||||
|     "supportedSourcesBelow": "Fuentes Soportadas:", | ||||
|     "supportedSources": "Fuentes Soportadas", | ||||
|     "trackOnlyInBrackets": "(Solo Seguimiento)", | ||||
|     "searchableInBrackets": "(Soporta Búsquedas)", | ||||
|     "appsString": "Aplicaciones", | ||||
| @@ -246,6 +238,23 @@ | ||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "¿Eliminar Aplicación?", | ||||
|         "other": "¿Eliminar Aplicaciones?" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} y 1 aplicación más han sido actualizadas.", | ||||
|         "other": "{} y {} aplicaciones más han sido actualizadas." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "خطای غیرمنتظره", | ||||
|     "ok": "باشه", | ||||
|     "and": "و", | ||||
|     "startedBgUpdateTask": "شروع بررسی بروزرسانی BG", | ||||
|     "bgUpdateIgnoreAfterIs": "نادیده گرفتن بروزرسانی BG بعد از {} است", | ||||
|     "startedActualBGUpdateCheck": "بررسی بهروزرسانی واقعی BG آغاز شد", | ||||
|     "bgUpdateTaskFinished": "کار بررسی بهروزرسانی BG تمام شد", | ||||
|     "firstRun": "این اولین اجرای Obtainium است", | ||||
|     "settingUpdateCheckIntervalTo": "تنظیم فاصله بهروزرسانی روی {}", | ||||
|     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", | ||||
|     "githubPATHint": "PAT باید در این قالب باشد: username:token", | ||||
|     "githubPATFormat": "username:token", | ||||
|     "includePrereleases": "شامل نسخه های اولیه", | ||||
|     "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", | ||||
|     "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", | ||||
|     "search": "جستجو کردن", | ||||
|     "additionalOptsFor": "گزینه های اضافی برای {}", | ||||
|     "supportedSourcesBelow": "منابع پشتیبانی شده:", | ||||
|     "supportedSources": "منابع پشتیبانی شده", | ||||
|     "trackOnlyInBrackets": "«فقط ردیابی»", | ||||
|     "searchableInBrackets": "(قابل جستجو)", | ||||
|     "appsString": "برنامه ها", | ||||
| @@ -229,23 +221,40 @@ | ||||
|     "dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید", | ||||
|     "dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید", | ||||
|     "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید", | ||||
|     "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو را فعال می کند and Better APK Discovery)", | ||||
|     "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)", | ||||
|     "about": "درباره", | ||||
|     "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", | ||||
|     "checkOnStart": "بررسی در شروع", | ||||
|     "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید", | ||||
|     "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", | ||||
|     "pickHighestVersionCode": "Auto-select highest version code APK", | ||||
|     "checkUpdateOnDetailPage": "Check for updates on opening an App detail page", | ||||
|     "disablePageTransitions": "Disable page transition animations", | ||||
|     "reversePageTransitions": "Reverse page transition animations", | ||||
|     "minStarCount": "Minimum Star Count", | ||||
|     "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", | ||||
|     "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی", | ||||
|     "pickHighestVersionCode": "انتخاب خودکار بالاترین کد نسخه APK", | ||||
|     "checkUpdateOnDetailPage": "برای باز کردن صفحه جزئیات برنامه، بهروزرسانیها را بررسی کنید", | ||||
|     "disablePageTransitions": "غیرفعال کردن انیمیشن های انتقال صفحه", | ||||
|     "reversePageTransitions": "انیمیشن های انتقال معکوس صفحه", | ||||
|     "minStarCount": "حداقل تعداد ستاره", | ||||
|     "addInfoBelow": "این اطلاعات را در زیر اضافه کنید", | ||||
|     "addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.", | ||||
|     "githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.", | ||||
|     "gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.", | ||||
|     "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل", | ||||
|     "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید", | ||||
|     "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "برنامه حذف شود؟", | ||||
|         "other": "برنامه ها حذف شوند؟" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} و 1 برنامه دیگر به روز شدند.", | ||||
|         "other": "{} و {} برنامه دیگر به روز شدند." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Erreur inattendue", | ||||
|     "ok": "Okay", | ||||
|     "and": "et", | ||||
|     "startedBgUpdateTask": "Démarrage de la tâche de vérification de mise à jour en arrière-plan", | ||||
|     "bgUpdateIgnoreAfterIs": "Mise à jour en arrière-plan est ignoré après  {}", | ||||
|     "startedActualBGUpdateCheck": "Démarrage de la vérification de la mise à jour en arrière-plan", | ||||
|     "bgUpdateTaskFinished": "Tâche de vérification de la mise à jour en arrière-plan terminée", | ||||
|     "firstRun": "Il s'agit de la toute première exécution d'Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Définition de l'intervalle de mise à jour sur {}", | ||||
|     "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", | ||||
|     "githubPATHint": "Le JAP doit être dans ce format : username:token", | ||||
|     "githubPATFormat": "username:token", | ||||
|     "includePrereleases": "Inclure les avant-premières", | ||||
|     "fallbackToOlderReleases": "Retour aux anciennes versions", | ||||
|     "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", | ||||
|     "search": "Rechercher", | ||||
|     "additionalOptsFor": "Options supplémentaires pour {}", | ||||
|     "supportedSourcesBelow": "Sources prises en charge :", | ||||
|     "supportedSources": "Sources prises en charge ", | ||||
|     "trackOnlyInBrackets": "(Suivi uniquement)", | ||||
|     "searchableInBrackets": "(Recherchable)", | ||||
|     "appsString": "Applications", | ||||
| @@ -246,6 +238,23 @@ | ||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Supprimer l'application ?", | ||||
|         "other": "Supprimer les applications ?" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} et 1 autre application ont été mises à jour.", | ||||
|         "other": "{} et {} autres applications ont été mises à jour." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Váratlan hiba", | ||||
|     "ok": "Oké", | ||||
|     "and": "és", | ||||
|     "startedBgUpdateTask": "Háttérfrissítés ellenőrzési feladat elindítva", | ||||
|     "bgUpdateIgnoreAfterIs": "Háttérfrissítés ignoreAfter a következő: {}", | ||||
|     "startedActualBGUpdateCheck": "Elkezdődött a tényleges háttérfrissítés ellenőrzése", | ||||
|     "bgUpdateTaskFinished": "A háttérfrissítés ellenőrzési feladat befejeződött", | ||||
|     "firstRun": "Ez az Obtainium első futása", | ||||
|     "settingUpdateCheckIntervalTo": "A frissítési intervallum beállítása erre: {}", | ||||
|     "githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)", | ||||
|     "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", | ||||
|     "githubPATFormat": "felhasználónév:token", | ||||
|     "includePrereleases": "Tartalmazza az előzetes kiadásokat", | ||||
|     "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", | ||||
|     "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Keresés (csak egyes források)", | ||||
|     "search": "Keresés", | ||||
|     "additionalOptsFor": "További lehetőségek a következőhöz: {}", | ||||
|     "supportedSourcesBelow": "Támogatott források:", | ||||
|     "supportedSources": "Támogatott források", | ||||
|     "trackOnlyInBrackets": "(Csak nyomonkövetés)", | ||||
|     "searchableInBrackets": "(Kereshető)", | ||||
|     "appsString": "Appok", | ||||
| @@ -96,10 +88,10 @@ | ||||
|     "importExport": "Import/Export", | ||||
|     "settings": "Beállítások", | ||||
|     "exportedTo": "Exportálva ide {}", | ||||
|     "obtainiumExport": "Obtainium Export", | ||||
|     "obtainiumExport": "Obtainium Adat Exportálás", | ||||
|     "invalidInput": "Hibás bemenet", | ||||
|     "importedX": "Importálva innen {}", | ||||
|     "obtainiumImport": "Obtainium Import", | ||||
|     "obtainiumImport": "Obtainium Adat Importálás", | ||||
|     "importFromURLList": "Importálás URL listából", | ||||
|     "searchQuery": "Keresési lekérdezés", | ||||
|     "appURLList": "App URL lista", | ||||
| @@ -139,8 +131,8 @@ | ||||
|     "appSource": "App forrás", | ||||
|     "noLogs": "Nincsenek naplók", | ||||
|     "appLogs": "App naplók", | ||||
|     "close": "Bezár", | ||||
|     "share": "Megoszt", | ||||
|     "close": "Bezárás", | ||||
|     "share": "Megosztás", | ||||
|     "appNotFound": "App nem található", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||
|     "pickAnAPK": "Válasszon egy APK-t", | ||||
| @@ -222,7 +214,7 @@ | ||||
|     "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", | ||||
| @@ -231,20 +223,37 @@ | ||||
|     "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)", | ||||
|     "about": "Rólunk", | ||||
|     "requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)", | ||||
|     "checkOnStart": "Egyszer az indításkor", | ||||
|     "checkOnStart": "Egyszer az alkalmazás indításakor is", | ||||
|     "tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból", | ||||
|     "removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása", | ||||
|     "pickHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása", | ||||
|     "checkUpdateOnDetailPage": "Frissítések keresése az app részleteit tartalmazó oldal megnyitásakor", | ||||
|     "disablePageTransitions": "Lap áttűnési animációk tiltása", | ||||
|     "disablePageTransitions": "Lap áttűnési animációk letiltása", | ||||
|     "reversePageTransitions": "Fordított lap áttűnési animációk", | ||||
|     "minStarCount": "Minimális csillag szám", | ||||
|     "addInfoBelow": "Adja hozzá ezt az infót alább.", | ||||
|     "addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.", | ||||
|     "githubSourceNote": "A GitHub értékelési korlátozása elkerülhető API-kulcs használatával.", | ||||
|     "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": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett", | ||||
|     "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel", | ||||
|     "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')", | ||||
|     "appsPossiblyUpdated": "App frissítési kísérlet", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben", | ||||
|     "xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.", | ||||
|     "backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.", | ||||
|     "verifyLatestTag": "Ellenőrizze a „legújabb” címkét", | ||||
|     "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)", | ||||
|     "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Eltávolítja az alkalmazást?", | ||||
|         "other": "Eltávolítja az alkalmazást?" | ||||
| @@ -287,10 +296,14 @@ | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "A(z) {} és 1 további alkalmazás frissítéseket kapott.", | ||||
|         "other": "{} és további {} alkalmazás frissítéseket kapott." | ||||
|         "other": "{} és {} további alkalmazás frissítéseket kapott." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "A(z) {} és 1 további alkalmazás frissítve.", | ||||
|         "other": "{} és további {} alkalmazás frissítve." | ||||
|         "other": "{} és {} további alkalmazás frissítve." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} és 1 további alkalmazás is frissült.", | ||||
|         "other": "{} és {} további alkalmazás is frissült." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Errore imprevisto", | ||||
|     "ok": "Va bene", | ||||
|     "and": "e", | ||||
|     "startedBgUpdateTask": "Avviata l'attività di controllo degli aggiornamenti in secondo piano", | ||||
|     "bgUpdateIgnoreAfterIs": "Il parametro di agg. in secondo piano 'ignoreAfter' è {}", | ||||
|     "startedActualBGUpdateCheck": "Avviato il controllo effettivo degli aggiornamenti in secondo piano", | ||||
|     "bgUpdateTaskFinished": "Terminata l'attività di controllo degli aggiornamenti in secondo piano", | ||||
|     "firstRun": "Questo è il primo avvio di sempre di Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Fissato intervallo di aggiornamento a {}", | ||||
|     "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", | ||||
|     "githubPATHint": "PAT deve seguire questo formato: nomeutente:token", | ||||
|     "githubPATFormat": "nomeutente:token", | ||||
|     "includePrereleases": "Includi prerelease", | ||||
|     "fallbackToOlderReleases": "Ripiega su release precedenti", | ||||
|     "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", | ||||
|     "search": "Cerca", | ||||
|     "additionalOptsFor": "Opzioni aggiuntive per {}", | ||||
|     "supportedSourcesBelow": "Fonti supportate:", | ||||
|     "supportedSources": "Fonti supportate", | ||||
|     "trackOnlyInBrackets": "(Solo-Monitoraggio)", | ||||
|     "searchableInBrackets": "(ricercabile)", | ||||
|     "appsString": "App", | ||||
| @@ -246,6 +238,23 @@ | ||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Rimuovere l'app?", | ||||
|         "other": "Rimuovere le app?" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} e un'altra app sono state aggiornate.", | ||||
|         "other": "{} e altre {} app sono state aggiornate." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,18 +11,10 @@ | ||||
|     "unexpectedError": "予期せぬエラーが発生しました", | ||||
|     "ok": "OK", | ||||
|     "and": "と", | ||||
|     "startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始", | ||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}", | ||||
|     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", | ||||
|     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", | ||||
|     "firstRun": "これがObtainiumの最初の実行です", | ||||
|     "settingUpdateCheckIntervalTo": "確認間隔を{}に設定する", | ||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", | ||||
|     "githubPATFormat": "ユーザー名:トークン", | ||||
|     "includePrereleases": "プレリリースを含む", | ||||
|     "fallbackToOlderReleases": "旧リリースへのフォールバック", | ||||
|     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む", | ||||
|     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", | ||||
|     "invalidRegEx": "無効な正規表現", | ||||
|     "noDescription": "説明はありません", | ||||
|     "cancel": "キャンセル", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "検索 (一部ソースのみ)", | ||||
|     "search": "検索", | ||||
|     "additionalOptsFor": "{}の追加オプション", | ||||
|     "supportedSourcesBelow": "対応するソース:", | ||||
|     "supportedSources": "対応するソース", | ||||
|     "trackOnlyInBrackets": "(追跡のみ)", | ||||
|     "searchableInBrackets": "(検索可能)", | ||||
|     "appsString": "アプリ", | ||||
| @@ -88,7 +80,7 @@ | ||||
|     "showOutdatedOnly": "アップデートが存在するアプリのみ表示する", | ||||
|     "filter": "フィルター", | ||||
|     "filterActive": "フィルター *", | ||||
|     "filterApps": "アプリを絞り込む", | ||||
|     "filterApps": "アプリをフィルタリングする", | ||||
|     "appName": "アプリ名", | ||||
|     "author": "作者", | ||||
|     "upToDateApps": "最新のアプリ", | ||||
| @@ -152,7 +144,7 @@ | ||||
|     "updatesAvailable": "アップデートが利用可能", | ||||
|     "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", | ||||
|     "noNewUpdates": "新しいアップデートはありません", | ||||
|     "xHasAnUpdate": "{} のアップデートが利用可能です", | ||||
|     "xHasAnUpdate": "{} のアップデートが利用可能です。", | ||||
|     "appsUpdated": "アプリをアップデートしました", | ||||
|     "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", | ||||
|     "xWasUpdatedToY": "{} が {} にアップデートされました", | ||||
| @@ -211,7 +203,7 @@ | ||||
|     "copiedToClipboard": "クリップボードにコピーしました", | ||||
|     "storagePermissionDenied": "ストレージ権限が拒否されました", | ||||
|     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", | ||||
|     "filterAPKsByRegEx": "正規表現でAPKを絞り込む", | ||||
|     "filterAPKsByRegEx": "正規表現でAPKをフィルタリングする", | ||||
|     "removeFromObtainium": "Obtainiumから削除する", | ||||
|     "uninstallFromDevice": "デバイスからアンインストールする", | ||||
|     "onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。", | ||||
| @@ -244,15 +236,35 @@ | ||||
|     "addInfoInSettings": "設定でこの情報を追加してください。", | ||||
|     "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。", | ||||
|     "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする", | ||||
|     "filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする", | ||||
|     "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')", | ||||
|     "appsPossiblyUpdated": "アプリのアップデートを試行", | ||||
|     "appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する", | ||||
|     "xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります。", | ||||
|     "enableBackgroundUpdates": "バックグラウンドアップデートを有効化する", | ||||
|     "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。", | ||||
|     "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。", | ||||
|     "verifyLatestTag": "'latest'タグを確認する", | ||||
|     "intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする", | ||||
|     "intermediateLinkNotFound": "中間リンクが見つかりませんでした", | ||||
|     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", | ||||
|     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", | ||||
|     "autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する", | ||||
|     "versionExtractionRegEx": "バージョン抽出の正規表現", | ||||
|     "matchGroupToUse": "使用するマッチしたグループ", | ||||
|     "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする", | ||||
|     "pickExportDir": "エクスポートディレクトリを選択", | ||||
|     "autoExportOnChanges": "変更があった際に自動でエクスポートする", | ||||
|     "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする", | ||||
|     "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "アプリを削除しますか?", | ||||
|         "other": "アプリを削除しますか?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" | ||||
|         "one": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください", | ||||
|         "other": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください" | ||||
|     }, | ||||
|     "bgUpdateGotErrorRetryInMinutes": { | ||||
|         "one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します", | ||||
| @@ -263,35 +275,39 @@ | ||||
|         "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": "{} とさらに {} 個のアプリのアップデートが利用可能です", | ||||
|         "other": "{} とさらに {} 個のアプリのアップデートが利用可能です" | ||||
|         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です。", | ||||
|         "other": "{} とさらに {} 個のアプリのアップデートが利用可能です。" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} とさらに {} 個のアプリがアップデートされました", | ||||
|         "other": "{} とさらに {} 個のアプリがアップデートされました" | ||||
|         "one": "{} とさらに {} 個のアプリがアップデートされました。", | ||||
|         "other": "{} とさらに {} 個のアプリがアップデートされました。" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。", | ||||
|         "other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,8 +4,10 @@ | ||||
|     "okay": "Okej", | ||||
|     "appId": "ID aplikacji", | ||||
|     "bgUpdateGotErrorRetryInMinutes": { | ||||
|         "one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min.", | ||||
|         "other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min." | ||||
|         "one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minutę", | ||||
|         "few": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty", | ||||
|         "many": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minut", | ||||
|         "other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minuty" | ||||
|     }, | ||||
|     "invalidURLForSource": "Nieprawidłowy adres URL aplikacji {}", | ||||
|     "noReleaseFound": "Nie można znaleźć odpowiedniego wydania", | ||||
| @@ -19,15 +21,7 @@ | ||||
|     "unexpectedError": "Nieoczekiwany błąd", | ||||
|     "ok": "Okej", | ||||
|     "and": "i", | ||||
|     "startedBgUpdateTask": "Rozpoczęto zadanie sprawdzania aktualizacji w tle", | ||||
|     "bgUpdateIgnoreAfterIs": "Parametr ignoreAfter aktualizacji w tle to {}", | ||||
|     "startedActualBGUpdateCheck": "Rozpoczęto sprawdzanie aktualizacji w tle", | ||||
|     "bgUpdateTaskFinished": "Zakończono zadanie sprawdzania aktualizacji w tle", | ||||
|     "firstRun": "Jest to pierwsze uruchomienie Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Ustawianie interwału aktualizacji na {}", | ||||
|     "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)", | ||||
|     "githubPATHint": "Wymagany format: użytkownik:token", | ||||
|     "githubPATFormat": "użytkownik:token", | ||||
|     "includePrereleases": "Uwzględnij wersje wstępne", | ||||
|     "fallbackToOlderReleases": "Powracaj do starszych wersji", | ||||
|     "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", | ||||
| @@ -56,7 +50,7 @@ | ||||
|     "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", | ||||
|     "search": "Szukaj", | ||||
|     "additionalOptsFor": "Dodatkowe opcje dla {}", | ||||
|     "supportedSourcesBelow": "Obsługiwane źródła:", | ||||
|     "supportedSources": "Obsługiwane źródła", | ||||
|     "trackOnlyInBrackets": "(tylko obserwowane)", | ||||
|     "searchableInBrackets": "(Wyszukiwalne)", | ||||
|     "appsString": "Aplikacje", | ||||
| @@ -248,50 +242,98 @@ | ||||
|     "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": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków", | ||||
|     "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego", | ||||
|     "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")", | ||||
|     "appsPossiblyUpdated": "Informuj o próbach aktualizacji", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Powiadamiaj o potencjalnym zastosowaniu w tle aktualizacji jednej lub większej ilości aplikacji", | ||||
|     "xWasPossiblyUpdatedToY": "{} być może zaktualizowano do {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.", | ||||
|     "backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.", | ||||
|     "verifyLatestTag": "Zweryfikuj najnowszy tag", | ||||
|     "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)", | ||||
|     "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi", | ||||
|     "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK", | ||||
|     "versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję", | ||||
|     "matchGroupToUse": "Dopasuj grupę do użycia", | ||||
|     "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe", | ||||
|     "pickExportDir": "Wybierz katalog eksportu", | ||||
|     "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Usunąć aplikację?", | ||||
|         "few": "Usunąć aplikacje?", | ||||
|         "many": "Usunąć aplikacje?", | ||||
|         "other": "Usunąć aplikacje?" | ||||
|     }, | ||||
|     "tooManyRequestsTryAgainInMinutes": { | ||||
|         "one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min.", | ||||
|         "other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min." | ||||
|         "one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minutę", | ||||
|         "few": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty", | ||||
|         "many": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minut", | ||||
|         "other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minuty" | ||||
|     }, | ||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||
|         "one": "Podczas sprawdzania aktualizacji w tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony", | ||||
|         "other": "Podczas sprawdzania aktualizacji w tle znaleziono {} akt. - w razie potrzeby użytkownik zostanie o tym powiadomiony" | ||||
|         "one": "W tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony", | ||||
|         "few": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony", | ||||
|         "many": "W tle znaleziono {} aktualizacji - w razie potrzeby użytkownik zostanie o tym powiadomiony", | ||||
|         "other": "W tle znaleziono {} aktualizacje - w razie potrzeby użytkownik zostanie o tym powiadomiony" | ||||
|     }, | ||||
|     "apps": { | ||||
|         "one": "{} aplik.", | ||||
|         "other": "{} aplik." | ||||
|         "one": "{} apkę", | ||||
|         "few": "{} apki", | ||||
|         "many": "{} apek", | ||||
|         "other": "{} apki" | ||||
|     }, | ||||
|     "url": { | ||||
|         "one": "{} adres URL", | ||||
|         "other": "{} adr. URL" | ||||
|         "few": "{} adresy URL", | ||||
|         "many": "{} adresów URL", | ||||
|         "other": "{} adresy URL" | ||||
|     }, | ||||
|     "minute": { | ||||
|         "one": "{} min.", | ||||
|         "other": "{} min." | ||||
|         "one": "{} minuta", | ||||
|         "few": "{} minuty", | ||||
|         "many": "{} minut", | ||||
|         "other": "{} minuty" | ||||
|     }, | ||||
|     "hour": { | ||||
|         "one": "{} godz.", | ||||
|         "other": "{} godz." | ||||
|         "one": "{} godzina", | ||||
|         "few": "{} godziny", | ||||
|         "many": "{} godzin", | ||||
|         "other": "{} godziny" | ||||
|     }, | ||||
|     "day": { | ||||
|         "one": "{} dzień", | ||||
|         "few": "{} dni", | ||||
|         "many": "{} dni", | ||||
|         "other": "{} dni" | ||||
|     }, | ||||
|     "clearedNLogsBeforeXAfterY": { | ||||
|         "one": "Wyczyszczono {n} log (przed = {before}, po = {after})", | ||||
|         "other": "Wyczyszczono logi: {n} (przed = {before}, po = {after})" | ||||
|         "few": "Wyczyszczono {n} logi (przed = {before}, po = {after})", | ||||
|         "many": "Wyczyszczono {n} logów (przed = {before}, po = {after})", | ||||
|         "other": "Wyczyszczono {n} logi (przed = {before}, po = {after})" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesAvailable": { | ||||
|         "one": "{} i jeszcze 1 aplikacja mają aktualizacje.", | ||||
|         "other": "{} i {} aplik. otrzymało aktualizacje." | ||||
|         "one": "{} i 1 inna apka mają aktualizacje.", | ||||
|         "few": "{} i {} inne apki mają aktualizacje.", | ||||
|         "many": "{} i {} innych apek ma aktualizacje.", | ||||
|         "other": "{} i {} inne apki mają aktualizacje." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "Zaktualizowano {} i jeszcze 1 aplikację.", | ||||
|         "other": "Zaktualizowano {} i {} aplik." | ||||
|     } | ||||
| } | ||||
|         "one": "Zaktualizowano {} i 1 inną apkę.", | ||||
|         "few": "{} i {} inne apki zostały zaktualizowane.", | ||||
|         "many": "{} i {} innych apek zostało zaktualizowanych.", | ||||
|         "other": "{} i {} inne apki zostały zaktualizowane." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} i 1 inna apka mogły zostać zaktualizowane.", | ||||
|         "few": "{} i {} inne apki mogły zostać zaktualizowane.", | ||||
|         "many": "{} i {} innych apek mogło zostać zaktualizowanych.", | ||||
|         "other": "{} i {} inne apki mogły zostać zaktualizowane." | ||||
|     }, | ||||
|     "enableBackgroundUpdates": "Włącz aktualizacje w tle", | ||||
|     "intermediateLinkRegex": "Filtr linków \"pośrednich\" do odwiedzenia w pierwszej kolejności", | ||||
|     "intermediateLinkNotFound": "Nie znaleziono linku pośredniego" | ||||
| } | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "Неожиданная ошибка", | ||||
|     "ok": "Окей", | ||||
|     "and": "и", | ||||
|     "startedBgUpdateTask": "Запущена задача фоновой проверки обновлений", | ||||
|     "bgUpdateIgnoreAfterIs": "Параметр игнорирования фоновых обновлений: {}", | ||||
|     "startedActualBGUpdateCheck": "Запущена фактическая проверка фоновых обновлений", | ||||
|     "bgUpdateTaskFinished": "Завершена задача фоновой проверки обновлений", | ||||
|     "firstRun": "Это первый запуск Obtainium", | ||||
|     "settingUpdateCheckIntervalTo": "Установка интервала проверки обновлений: {}", | ||||
|     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", | ||||
|     "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен", | ||||
|     "githubPATFormat": "имя_пользователя:токен", | ||||
|     "includePrereleases": "Включить предварительные релизы", | ||||
|     "fallbackToOlderReleases": "Откатиться к более старым версиям", | ||||
|     "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", | ||||
| @@ -49,7 +41,7 @@ | ||||
|     "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", | ||||
|     "search": "Поиск", | ||||
|     "additionalOptsFor": "Дополнительные опции для {}", | ||||
|     "supportedSourcesBelow": "Поддерживаемые источники:", | ||||
|     "supportedSources": "Поддерживаемые источники", | ||||
|     "trackOnlyInBrackets": "(Только для отслеживания)", | ||||
|     "searchableInBrackets": "(Поиск)", | ||||
|     "appsString": "Приложения", | ||||
| @@ -246,6 +238,23 @@ | ||||
|     "gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "Удалить приложение?", | ||||
|         "other": "Удалить приложения?" | ||||
| @@ -293,5 +302,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} и еще 1 приложение были обновлены.", | ||||
|         "other": "{} и еще {} приложений были обновлены." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,15 +11,7 @@ | ||||
|     "unexpectedError": "意外错误", | ||||
|     "ok": "好的", | ||||
|     "and": "和", | ||||
|     "startedBgUpdateTask": "后台更新检查任务已启动", | ||||
|     "bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}", | ||||
|     "startedActualBGUpdateCheck": "开始后台更新检查", | ||||
|     "bgUpdateTaskFinished": "后台更新检查任务已完成", | ||||
|     "firstRun": "这是 Obtainium 首次启动", | ||||
|     "settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}", | ||||
|     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", | ||||
|     "githubPATHint": "个人访问令牌必须为“username:token”的格式", | ||||
|     "githubPATFormat": "username:token", | ||||
|     "includePrereleases": "包含预发行版", | ||||
|     "fallbackToOlderReleases": "将旧发行版作为备选", | ||||
|     "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", | ||||
| @@ -46,10 +38,10 @@ | ||||
|     "appSourceURL": "来源 URL", | ||||
|     "error": "错误", | ||||
|     "add": "添加", | ||||
|     "searchSomeSourcesLabel": "搜索(仅部分来源)", | ||||
|     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", | ||||
|     "search": "搜索", | ||||
|     "additionalOptsFor": "{} 的更多选项", | ||||
|     "supportedSourcesBelow": "支持的来源:", | ||||
|     "supportedSources": "支持的来源", | ||||
|     "trackOnlyInBrackets": "(仅追踪)", | ||||
|     "searchableInBrackets": "(可搜索)", | ||||
|     "appsString": "应用列表", | ||||
| @@ -150,16 +142,16 @@ | ||||
|     "warning": "警告", | ||||
|     "sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?", | ||||
|     "updatesAvailable": "更新可用", | ||||
|     "updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发出通知", | ||||
|     "updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发送通知", | ||||
|     "noNewUpdates": "全部应用已是最新。", | ||||
|     "xHasAnUpdate": "{} 可以更新了。", | ||||
|     "appsUpdated": "应用已更新", | ||||
|     "appsUpdatedNotifDescription": "当应用在后台安装更新时发出通知", | ||||
|     "appsUpdatedNotifDescription": "当应用在后台安装更新时发送通知", | ||||
|     "xWasUpdatedToY": "{} 已更新至 {}。", | ||||
|     "errorCheckingUpdates": "检查更新出错", | ||||
|     "errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知", | ||||
|     "appsRemoved": "应用已删除", | ||||
|     "appsRemovedNotifDescription": "当应用因加载出错而被删除时发出通知", | ||||
|     "appsRemovedNotifDescription": "当应用因加载出错而被删除时发送通知", | ||||
|     "xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}", | ||||
|     "completeAppInstallation": "完成应用安装", | ||||
|     "obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用", | ||||
| @@ -180,7 +172,7 @@ | ||||
|     "yesMarkUpdated": "是,标记为已更新", | ||||
|     "fdroid": "F-Droid 官方存储库", | ||||
|     "appIdOrName": "应用 ID 或名称", | ||||
|     "appId": "App ID", | ||||
|     "appId": "应用 ID", | ||||
|     "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用", | ||||
|     "reposHaveMultipleApps": "存储库中可能包含多个应用", | ||||
|     "fdroidThirdPartyRepo": "F-Droid 第三方存储库", | ||||
| @@ -244,8 +236,26 @@ | ||||
|     "addInfoInSettings": "在“设置”中添加此凭据。", | ||||
|     "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。", | ||||
|     "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。", | ||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||
|     "sortByFileNamesNotLinks": "使用文件名代替链接进行排序", | ||||
|     "filterReleaseNotesByRegEx": "使用正则表达式筛选发行说明", | ||||
|     "customLinkFilterRegex": "使用正则表达式自定义链接筛选(默认模式为“.apk$”)", | ||||
|     "appsPossiblyUpdated": "已尝试更新应用", | ||||
|     "appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知", | ||||
|     "xWasPossiblyUpdatedToY": "已尝试将 {} 更新至 {}。", | ||||
|     "enableBackgroundUpdates": "启用后台更新", | ||||
|     "backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。", | ||||
|     "backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。", | ||||
|     "verifyLatestTag": "验证“Latest”标签", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "pickExportDir": "Pick Export Directory", | ||||
|     "autoExportOnChanges": "Auto-export on changes", | ||||
|     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||
|     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "是否删除应用?", | ||||
|         "other": "是否删除应用?" | ||||
| @@ -259,8 +269,8 @@ | ||||
|         "other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试" | ||||
|     }, | ||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||
|         "one": "后台检查发现 {} 个应用更新 - 如有需要将发出通知", | ||||
|         "other": "后台检查发现 {} 个应用更新 - 如有需要将发出通知" | ||||
|         "one": "后台检查发现 {} 个应用更新 - 如有需要将发送通知", | ||||
|         "other": "后台检查发现 {} 个应用更新 - 如有需要将发送通知" | ||||
|     }, | ||||
|     "apps": { | ||||
|         "one": "{} 个应用", | ||||
| @@ -293,5 +303,9 @@ | ||||
|     "xAndNMoreUpdatesInstalled": { | ||||
|         "one": "{} 和另外 1 个应用已更新。", | ||||
|         "other": "{} 和另外 {} 个应用已更新。" | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} 和另外 1 个应用已尝试更新。", | ||||
|         "other": "{} 和另外 {} 个应用已尝试更新。" | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										20
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										20
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| #!/bin/bash | ||||
| # Convenience script | ||||
|  | ||||
| CURR_DIR="$(pwd)" | ||||
| trap "cd "$CURR_DIR"" EXIT | ||||
|  | ||||
| if [ -z "$1" ]; then | ||||
|     git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date | ||||
| fi | ||||
| rm ./build/app/outputs/flutter-apk/* 2>/dev/null                                             # Get rid of older builds if any | ||||
| flutter build apk && flutter build apk --split-per-abi                                       # Build (both split and combined APKs) | ||||
| for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures | ||||
| rsync -r ./build/app/outputs/flutter-apk/ ~/Downloads/Obtainium-build/                       # Dropoff in Downloads to allow for drag-drop into Flatpak Firefox | ||||
| cd ~/Downloads/Obtainium-build/                                                              # Make zips just in case (for in-comment uploads) | ||||
| for apk in *.apk; do | ||||
|     PREFIX="$(echo "$apk" | head -c -5)" | ||||
|     zip "$PREFIX" "$PREFIX"* | ||||
| done | ||||
| mkdir -p zips | ||||
| mv *.zip zips/ | ||||
| @@ -25,12 +25,16 @@ class APKCombo extends AppSource { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get requestHeaders => { | ||||
|         "User-Agent": "curl/8.0.1", | ||||
|         "Accept": "*/*", | ||||
|         "Connection": "keep-alive", | ||||
|         "Host": "$host" | ||||
|       }; | ||||
|   Future<Map<String, String>?> getRequestHeaders( | ||||
|       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||
|       bool forAPKDownload = false}) async { | ||||
|     return { | ||||
|       "User-Agent": "curl/8.0.1", | ||||
|       "Accept": "*/*", | ||||
|       "Connection": "keep-alive", | ||||
|       "Host": "$host" | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { | ||||
|     var res = await sourceRequest('$standardUrl/download/apk'); | ||||
|   | ||||
| @@ -3,6 +3,21 @@ import 'package:html/parser.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| parseDateTimeMMMddCommayyyy(String? dateString) { | ||||
|   DateTime? releaseDate; | ||||
|   try { | ||||
|     releaseDate = dateString != null | ||||
|         ? DateFormat('MMM dd, yyyy').parse(dateString) | ||||
|         : null; | ||||
|     releaseDate = dateString != null && releaseDate == null | ||||
|         ? DateFormat('MMMM dd, yyyy').parse(dateString) | ||||
|         : releaseDate; | ||||
|   } catch (err) { | ||||
|     // ignore | ||||
|   } | ||||
|   return releaseDate; | ||||
| } | ||||
|  | ||||
| class APKPure extends AppSource { | ||||
|   APKPure() { | ||||
|     host = 'apkpure.com'; | ||||
| @@ -47,17 +62,7 @@ class APKPure extends AppSource { | ||||
|       } | ||||
|       String? dateString = | ||||
|           html.querySelector('span.info-other span.date')?.text.trim(); | ||||
|       DateTime? releaseDate; | ||||
|       try { | ||||
|         releaseDate = dateString != null | ||||
|             ? DateFormat('MMM dd, yyyy').parse(dateString) | ||||
|             : null; | ||||
|         releaseDate = dateString != null && releaseDate == null | ||||
|             ? DateFormat('MMMM dd, yyyy').parse(dateString) | ||||
|             : null; | ||||
|       } catch (err) { | ||||
|         // ignore | ||||
|       } | ||||
|       DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString); | ||||
|       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; | ||||
|       List<MapEntry<String, String>> apkUrls = [ | ||||
|         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') | ||||
| @@ -70,11 +75,13 @@ class APKPure extends AppSource { | ||||
|           Uri.parse(standardUrl).pathSegments.reversed.last; | ||||
|       String appName = | ||||
|           html.querySelector('h1.info-title')?.text.trim() ?? appId; | ||||
|       String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml | ||||
|           .trim().replaceAll("<br>", "  \n"); | ||||
|       String? changeLog = htmlChangelog | ||||
|           .querySelector("div.whats-new-info p:not(.date)") | ||||
|           ?.innerHtml | ||||
|           .trim() | ||||
|           .replaceAll("<br>", "  \n"); | ||||
|       return APKDetails(version, apkUrls, AppNames(author, appName), | ||||
|           releaseDate: releaseDate, | ||||
|           changeLog: changeLog); | ||||
|           releaseDate: releaseDate, changeLog: changeLog); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class Aptoide extends AppSource { | ||||
|   Aptoide() { | ||||
|     host = 'aptoide.com'; | ||||
|     name = tr('Aptoide'); | ||||
|     allowSubDomains = true; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
|     } | ||||
|     return url.substring(0, match.end); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<String?> tryInferringAppId(String standardUrl, | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     return (await getAppDetailsJSON(standardUrl))['package']; | ||||
|   } | ||||
|  | ||||
|   Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async { | ||||
|     var res = await sourceRequest(standardUrl); | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body); | ||||
|     String? id; | ||||
|     if (idMatch != null) { | ||||
|       id = res.body.substring(idMatch.start + 12, idMatch.end); | ||||
|     } else { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     var res2 = | ||||
|         await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id'); | ||||
|     if (res2.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     return jsonDecode(res2.body)?['nodes']?['meta']?['data']; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var appDetails = await getAppDetailsJSON(standardUrl); | ||||
|     String appName = appDetails['name'] ?? tr('app'); | ||||
|     String author = appDetails['developer']?['name'] ?? name; | ||||
|     String? dateStr = appDetails['updated']; | ||||
|     String? version = appDetails['file']?['vername']; | ||||
|     String? apkUrl = appDetails['file']?['path']; | ||||
|     if (version == null) { | ||||
|       throw NoVersionError(); | ||||
|     } | ||||
|     if (apkUrl == null) { | ||||
|       throw NoAPKError(); | ||||
|     } | ||||
|     DateTime? relDate; | ||||
|     if (dateStr != null) { | ||||
|       relDate = DateTime.parse(dateStr); | ||||
|     } | ||||
|  | ||||
|     return APKDetails( | ||||
|         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||
|         releaseDate: relDate); | ||||
|   } | ||||
| } | ||||
| @@ -3,6 +3,7 @@ import 'dart:convert'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -11,6 +12,26 @@ class FDroid extends AppSource { | ||||
|     host = 'f-droid.org'; | ||||
|     name = tr('fdroid'); | ||||
|     canSearch = true; | ||||
|     additionalSourceAppSpecificSettingFormItems = [ | ||||
|       [ | ||||
|         GeneratedFormTextField('filterVersionsByRegEx', | ||||
|             label: tr('filterVersionsByRegEx'), | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 return regExValidator(value); | ||||
|               } | ||||
|             ]) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormSwitch('trySelectingSuggestedVersionCode', | ||||
|             label: tr('trySelectingSuggestedVersionCode'), defaultValue: true) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormSwitch('autoSelectHighestVersionCode', | ||||
|             label: tr('autoSelectHighestVersionCode')) | ||||
|       ], | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -37,21 +58,74 @@ class FDroid extends AppSource { | ||||
|   } | ||||
|  | ||||
|   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|       Response res, String apkUrlPrefix, String standardUrl) { | ||||
|       Response res, String apkUrlPrefix, String standardUrl, | ||||
|       {bool autoSelectHighestVersionCode = false, | ||||
|       bool trySelectingSuggestedVersionCode = false, | ||||
|       String? filterVersionsByRegEx}) { | ||||
|     if (res.statusCode == 200) { | ||||
|       List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; | ||||
|       var response = jsonDecode(res.body); | ||||
|       List<dynamic> releases = response['packages'] ?? []; | ||||
|       if (releases.isEmpty) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       String? latestVersion = releases[0]['versionName']; | ||||
|       if (latestVersion == null) { | ||||
|       String? version; | ||||
|       Iterable<dynamic> releaseChoices = []; | ||||
|       // Grab the versionCode suggested if the user chose to do that | ||||
|       // Only do so at this stage if the user has no release filter | ||||
|       if (trySelectingSuggestedVersionCode && | ||||
|           response['suggestedVersionCode'] != null && | ||||
|           filterVersionsByRegEx == null) { | ||||
|         var suggestedReleases = releases.where((element) => | ||||
|             element['versionCode'] == response['suggestedVersionCode']); | ||||
|         if (suggestedReleases.isNotEmpty) { | ||||
|           releaseChoices = suggestedReleases; | ||||
|           version = suggestedReleases.first['versionName']; | ||||
|         } | ||||
|       } | ||||
|       // Apply the release filter if any | ||||
|       if (filterVersionsByRegEx != null) { | ||||
|         version = null; | ||||
|         releaseChoices = []; | ||||
|         for (var i = 0; i < releases.length; i++) { | ||||
|           if (RegExp(filterVersionsByRegEx) | ||||
|               .hasMatch(releases[i]['versionName'])) { | ||||
|             version = releases[i]['versionName']; | ||||
|           } | ||||
|         } | ||||
|         if (version == null) { | ||||
|           throw NoVersionError(); | ||||
|         } | ||||
|       } | ||||
|       // Default to the highest version | ||||
|       version ??= releases[0]['versionName']; | ||||
|       if (version == null) { | ||||
|         throw NoVersionError(); | ||||
|       } | ||||
|       List<String> apkUrls = releases | ||||
|           .where((element) => element['versionName'] == latestVersion) | ||||
|       // If a suggested release was not already picked, pick all those with the selected version | ||||
|       if (releaseChoices.isEmpty) { | ||||
|         releaseChoices = | ||||
|             releases.where((element) => element['versionName'] == version); | ||||
|       } | ||||
|       // For the remaining releases, use the toggles to auto-select one if possible | ||||
|       if (releaseChoices.length > 1) { | ||||
|         if (autoSelectHighestVersionCode) { | ||||
|           releaseChoices = [releaseChoices.first]; | ||||
|         } else if (trySelectingSuggestedVersionCode && | ||||
|             response['suggestedVersionCode'] != null) { | ||||
|           var suggestedReleases = releaseChoices.where((element) => | ||||
|               element['versionCode'] == response['suggestedVersionCode']); | ||||
|           if (suggestedReleases.isNotEmpty) { | ||||
|             releaseChoices = suggestedReleases; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       if (releaseChoices.isEmpty) { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       List<String> apkUrls = releaseChoices | ||||
|           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') | ||||
|           .toList(); | ||||
|       return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), | ||||
|       return APKDetails(version, getApkUrlsFromUrls(apkUrls), | ||||
|           AppNames(name, Uri.parse(standardUrl).pathSegments.last)); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
| @@ -68,7 +142,17 @@ class FDroid extends AppSource { | ||||
|     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|         await sourceRequest('https://$host/api/v1/packages/$appId'), | ||||
|         'https://$host/repo/$appId', | ||||
|         standardUrl); | ||||
|         standardUrl, | ||||
|         autoSelectHighestVersionCode: | ||||
|             additionalSettings['autoSelectHighestVersionCode'] == true, | ||||
|         trySelectingSuggestedVersionCode: | ||||
|             additionalSettings['trySelectingSuggestedVersionCode'] == true, | ||||
|         filterVersionsByRegEx: | ||||
|             (additionalSettings['filterVersionsByRegEx'] as String?) | ||||
|                         ?.isNotEmpty == | ||||
|                     true | ||||
|                 ? additionalSettings['filterVersionsByRegEx'] | ||||
|                 : null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| @@ -21,20 +22,6 @@ class GitHub extends AppSource { | ||||
|           label: tr('githubPATLabel'), | ||||
|           password: true, | ||||
|           required: false, | ||||
|           additionalValidators: [ | ||||
|             (value) { | ||||
|               if (value != null && value.trim().isNotEmpty) { | ||||
|                 if (value | ||||
|                         .split(':') | ||||
|                         .where((element) => element.trim().isNotEmpty) | ||||
|                         .length != | ||||
|                     2) { | ||||
|                   return tr('githubPATHint'); | ||||
|                 } | ||||
|               } | ||||
|               return null; | ||||
|             } | ||||
|           ], | ||||
|           hint: tr('githubPATFormat'), | ||||
|           belowWidgets: [ | ||||
|             const SizedBox( | ||||
| @@ -85,6 +72,10 @@ class GitHub extends AppSource { | ||||
|                 return regExValidator(value); | ||||
|               } | ||||
|             ]) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormSwitch('verifyLatestTag', | ||||
|             label: tr('verifyLatestTag'), defaultValue: false) | ||||
|       ] | ||||
|     ]; | ||||
|  | ||||
| @@ -165,26 +156,53 @@ class GitHub extends AppSource { | ||||
|     return url.substring(0, match.end); | ||||
|   } | ||||
|  | ||||
|   Future<String> getCredentialPrefixIfAny( | ||||
|       Map<String, dynamic> additionalSettings) async { | ||||
|   @override | ||||
|   Future<Map<String, String>?> getRequestHeaders( | ||||
|       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||
|       bool forAPKDownload = false}) async { | ||||
|     var token = await getTokenIfAny(additionalSettings); | ||||
|     var headers = <String, String>{}; | ||||
|     if (token != null) { | ||||
|       headers[HttpHeaders.authorizationHeader] = 'Token $token'; | ||||
|     } | ||||
|     if (forAPKDownload == true) { | ||||
|       headers[HttpHeaders.acceptHeader] = 'application/octet-stream'; | ||||
|     } | ||||
|     if (headers.isNotEmpty) { | ||||
|       return headers; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async { | ||||
|     SettingsProvider settingsProvider = SettingsProvider(); | ||||
|     await settingsProvider.initializeSettings(); | ||||
|     var sourceConfig = | ||||
|         await getSourceConfigValues(additionalSettings, settingsProvider); | ||||
|     String? creds = sourceConfig['github-creds']; | ||||
|     return creds != null && creds.isNotEmpty ? '$creds@' : ''; | ||||
|     if (creds != null) { | ||||
|       var userNameEndIndex = creds.indexOf(':'); | ||||
|       if (userNameEndIndex > 0) { | ||||
|         creds = creds.substring( | ||||
|             userNameEndIndex + 1); // For old username-included token inputs | ||||
|       } | ||||
|       return creds; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<String?> getSourceNote() async { | ||||
|     if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) { | ||||
|     if (!hostChanged && (await getTokenIfAny({})) == null) { | ||||
|       return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => | ||||
|       'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host'; | ||||
|       'https://api.$host'; | ||||
|  | ||||
|   Future<String> convertStandardUrlToAPIUrl( | ||||
|           String standardUrl, Map<String, dynamic> additionalSettings) async => | ||||
| @@ -212,6 +230,21 @@ class GitHub extends AppSource { | ||||
|                 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>; | ||||
| @@ -219,9 +252,10 @@ class GitHub extends AppSource { | ||||
|       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => | ||||
|           (release['assets'] as List<dynamic>?) | ||||
|               ?.map((e) { | ||||
|                 return e['name'] != null && e['browser_download_url'] != null | ||||
|                 return (e['name'] != null) && | ||||
|                         ((e['url'] ?? e['browser_download_url']) != null) | ||||
|                     ? MapEntry(e['name'] as String, | ||||
|                         e['browser_download_url'] as String) | ||||
|                         (e['url'] ?? e['browser_download_url']) as String) | ||||
|                     : const MapEntry('', ''); | ||||
|               }) | ||||
|               .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||
| @@ -258,6 +292,17 @@ class GitHub extends AppSource { | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       if (latestTag != null && | ||||
|           releases.isNotEmpty && | ||||
|           latestTag != | ||||
|               (releases[releases.length - 1]['tag_name'] ?? | ||||
|                   releases[0]['name'])) { | ||||
|         var ind = releases.indexWhere( | ||||
|             (element) => latestTag == (element['tag_name'] ?? element['name'])); | ||||
|         if (ind >= 0) { | ||||
|           releases.add(releases.removeAt(ind)); | ||||
|         } | ||||
|       } | ||||
|       releases = releases.reversed.toList(); | ||||
|       dynamic targetRelease; | ||||
|       var prerrelsSkipped = 0; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| @@ -18,7 +19,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { | ||||
|       .toList(); | ||||
|   if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { | ||||
|     return '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; | ||||
|   } else if (ambiguousUrl.split('/').length == 1) { | ||||
|   } else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) { | ||||
|     return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl'; | ||||
|   } else { | ||||
|     return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl'; | ||||
| @@ -91,16 +92,56 @@ class HTML extends AppSource { | ||||
|       [ | ||||
|         GeneratedFormSwitch('sortByFileNamesNotLinks', | ||||
|             label: tr('sortByFileNamesNotLinks')) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormTextField('customLinkFilterRegex', | ||||
|             label: tr('customLinkFilterRegex'), | ||||
|             hint: 'download/(.*/)?(android|apk|mobile)', | ||||
|             required: false, | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 return regExValidator(value); | ||||
|               } | ||||
|             ]) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormTextField('intermediateLinkRegex', | ||||
|             label: tr('intermediateLinkRegex'), | ||||
|             hint: '([0-9]+\.)*[0-9]+/\$', | ||||
|             required: false, | ||||
|             additionalValidators: [(value) => regExValidator(value)]) | ||||
|       ], | ||||
|       [ | ||||
|         GeneratedFormTextField('versionExtractionRegEx', | ||||
|             label: tr('versionExtractionRegEx'), | ||||
|             required: false, | ||||
|             additionalValidators: [(value) => regExValidator(value)]), | ||||
|         GeneratedFormTextField('matchGroupToUse', | ||||
|             label: tr('matchGroupToUse'), | ||||
|             required: false, | ||||
|             hint: '1', | ||||
|             textInputType: const TextInputType.numberWithOptions(), | ||||
|             additionalValidators: [ | ||||
|               (value) { | ||||
|                 value ??= '1'; | ||||
|                 return intValidator(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" | ||||
|       }; | ||||
|   Future<Map<String, String>?> getRequestHeaders( | ||||
|       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||
|       bool forAPKDownload = false}) async { | ||||
|     return { | ||||
|       "User-Agent": | ||||
|           "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
| @@ -115,16 +156,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(); | ||||
|       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('/').last, b.split('/').last) | ||||
|           ? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last, | ||||
|               b.split('/').where((e) => e.isNotEmpty).last) | ||||
|           : compareAlphaNumeric(a, b)); | ||||
|       if (additionalSettings['apkFilterRegEx'] != null) { | ||||
|       if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == | ||||
|           true) { | ||||
|         var reg = RegExp(additionalSettings['apkFilterRegEx']); | ||||
|         links = links.where((element) => reg.hasMatch(element)).toList(); | ||||
|       } | ||||
| @@ -132,10 +201,23 @@ class HTML extends AppSource { | ||||
|         throw NoReleasesError(); | ||||
|       } | ||||
|       var rel = links.last; | ||||
|       var version = rel.hashCode.toString(); | ||||
|       String? version = rel.hashCode.toString(); | ||||
|       var versionExtractionRegEx = | ||||
|           additionalSettings['versionExtractionRegEx'] as String?; | ||||
|       if (versionExtractionRegEx?.isNotEmpty == true) { | ||||
|         var match = RegExp(versionExtractionRegEx!).allMatches(rel); | ||||
|         if (match.isEmpty) { | ||||
|           throw NoVersionError(); | ||||
|         } | ||||
|         version = match.last | ||||
|             .group(int.parse(additionalSettings['matchGroupToUse'] as String)); | ||||
|         if (version?.isEmpty == true) { | ||||
|           throw NoVersionError(); | ||||
|         } | ||||
|       } | ||||
|       List<String> apkUrls = | ||||
|           [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); | ||||
|       return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(), | ||||
|       return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(), | ||||
|           AppNames(uri.host, tr('app'))); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|   | ||||
							
								
								
									
										91
									
								
								lib/app_sources/huaweiappgallery.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								lib/app_sources/huaweiappgallery.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class HuaweiAppGallery extends AppSource { | ||||
|   HuaweiAppGallery() { | ||||
|     name = 'Huawei AppGallery'; | ||||
|     host = 'appgallery.huawei.com'; | ||||
|     overrideVersionDetectionFormDefault('releaseDateAsVersion', | ||||
|         disableStandard: true); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     RegExp standardUrlRegEx = RegExp('^https?://$host/app/[^/]+'); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
|     } | ||||
|     return url.substring(0, match.end); | ||||
|   } | ||||
|  | ||||
|   getDlUrl(String standardUrl) => | ||||
|       'https://${host!.replaceAll('appgallery.', 'appgallery.cloud.')}/appdl/${standardUrl.split('/').last}'; | ||||
|  | ||||
|   requestAppdlRedirect(String dlUrl) async { | ||||
|     Response res = await sourceRequest(dlUrl, followRedirects: false); | ||||
|     if (res.statusCode == 200 || | ||||
|         res.statusCode == 302 || | ||||
|         res.statusCode == 304) { | ||||
|       return res; | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   appIdFromRedirectDlUrl(String redirectDlUrl) { | ||||
|     var parts = redirectDlUrl | ||||
|         .split('?')[0] | ||||
|         .split('/') | ||||
|         .last | ||||
|         .split('.') | ||||
|         .reversed | ||||
|         .toList(); | ||||
|     parts.removeAt(0); | ||||
|     parts.removeAt(0); | ||||
|     return parts.reversed.join('.'); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<String?> tryInferringAppId(String standardUrl, | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     String dlUrl = getDlUrl(standardUrl); | ||||
|     Response res = await requestAppdlRedirect(dlUrl); | ||||
|     return res.headers['location'] != null | ||||
|         ? appIdFromRedirectDlUrl(res.headers['location']!) | ||||
|         : null; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     String dlUrl = getDlUrl(standardUrl); | ||||
|     Response res = await requestAppdlRedirect(dlUrl); | ||||
|     if (res.headers['location'] == null) { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     String appId = appIdFromRedirectDlUrl(res.headers['location']!); | ||||
|     var relDateStr = | ||||
|         res.headers['location']?.split('?')[0].split('.').reversed.toList()[1]; | ||||
|     var relDateStrAdj = relDateStr?.split(''); | ||||
|     var tempLen = relDateStrAdj?.length ?? 0; | ||||
|     var i = 2; | ||||
|     while (i < tempLen) { | ||||
|       relDateStrAdj?.insert((i + i ~/ 2 - 1), '-'); | ||||
|       i += 2; | ||||
|     } | ||||
|     var relDate = relDateStrAdj == null | ||||
|         ? null | ||||
|         : DateFormat('yy-MM-dd-HH-mm').parse(relDateStrAdj.join('')); | ||||
|     if (relDateStr == null) { | ||||
|       throw NoVersionError(); | ||||
|     } | ||||
|     return APKDetails( | ||||
|         relDateStr, [MapEntry('$appId.apk', dlUrl)], AppNames(name, appId), | ||||
|         releaseDate: relDate); | ||||
|   } | ||||
| } | ||||
| @@ -3,8 +3,13 @@ import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class IzzyOnDroid extends AppSource { | ||||
|   late FDroid fd; | ||||
|  | ||||
|   IzzyOnDroid() { | ||||
|     host = 'android.izzysoft.de'; | ||||
|     fd = FDroid(); | ||||
|     additionalSourceAppSpecificSettingFormItems = | ||||
|         fd.additionalSourceAppSpecificSettingFormItems; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -20,7 +25,7 @@ class IzzyOnDroid extends AppSource { | ||||
|   @override | ||||
|   Future<String?> tryInferringAppId(String standardUrl, | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     return FDroid().tryInferringAppId(standardUrl); | ||||
|     return fd.tryInferringAppId(standardUrl); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -29,10 +34,15 @@ class IzzyOnDroid extends AppSource { | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     String? appId = await tryInferringAppId(standardUrl); | ||||
|     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|     return fd.getAPKUrlsFromFDroidPackagesAPIResponse( | ||||
|         await sourceRequest( | ||||
|             'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), | ||||
|         'https://android.izzysoft.de/frepo/$appId', | ||||
|         standardUrl); | ||||
|         standardUrl, | ||||
|         autoSelectHighestVersionCode: | ||||
|             additionalSettings['autoSelectHighestVersionCode'] == true, | ||||
|         trySelectingSuggestedVersionCode: | ||||
|             additionalSettings['trySelectingSuggestedVersionCode'] == true, | ||||
|         filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,8 @@ import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class Jenkins extends AppSource { | ||||
|   Jenkins() { | ||||
|     overrideVersionDetectionFormDefault('releaseDateAsVersion', true); | ||||
|     overrideVersionDetectionFormDefault('releaseDateAsVersion', | ||||
|         disableStandard: true); | ||||
|   } | ||||
|  | ||||
|   String trimJobUrl(String url) { | ||||
|   | ||||
							
								
								
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:obtainium/app_sources/apkpure.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class Uptodown extends AppSource { | ||||
|   Uptodown() { | ||||
|     host = 'uptodown.com'; | ||||
|     allowSubDomains = true; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
|     } | ||||
|     return '${url.substring(0, match.end)}/android/download'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<String?> tryInferringAppId(String standardUrl, | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     return (await getAppDetailsFromPage(standardUrl))['appId']; | ||||
|   } | ||||
|  | ||||
|   Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async { | ||||
|     var res = await sourceRequest(standardUrl); | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var html = parse(res.body); | ||||
|     String? version = html.querySelector('div.version')?.innerHtml; | ||||
|     String? apkUrl = | ||||
|         html.querySelector('#detail-download-button')?.attributes['data-url']; | ||||
|     String? name = html.querySelector('#detail-app-name')?.innerHtml.trim(); | ||||
|     String? author = html.querySelector('#author-link')?.innerHtml.trim(); | ||||
|     var detailElements = html.querySelectorAll('#technical-information td'); | ||||
|     String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim(); | ||||
|     String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim(); | ||||
|     return Map.fromEntries([ | ||||
|       MapEntry('version', version), | ||||
|       MapEntry('apkUrl', apkUrl), | ||||
|       MapEntry('appId', appId), | ||||
|       MapEntry('name', name), | ||||
|       MapEntry('author', author), | ||||
|       MapEntry('dateStr', dateStr) | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var appDetails = await getAppDetailsFromPage(standardUrl); | ||||
|     var version = appDetails['version']; | ||||
|     var apkUrl = appDetails['apkUrl']; | ||||
|     var appId = appDetails['appId']; | ||||
|     if (version == null) { | ||||
|       throw NoVersionError(); | ||||
|     } | ||||
|     if (apkUrl == null) { | ||||
|       throw NoAPKError(); | ||||
|     } | ||||
|     if (appId == null) { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     String appName = appDetails['name'] ?? tr('app'); | ||||
|     String author = appDetails['author'] ?? name; | ||||
|     String? dateStr = appDetails['dateStr']; | ||||
|     DateTime? relDate; | ||||
|     if (dateStr != null) { | ||||
|       relDate = parseDateTimeMMMddCommayyyy(dateStr); | ||||
|     } | ||||
|     return APKDetails( | ||||
|         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||
|         releaseDate: relDate); | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/html.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -7,19 +9,46 @@ class VLC extends AppSource { | ||||
|   VLC() { | ||||
|     host = 'videolan.org'; | ||||
|   } | ||||
|   get dwUrlBase => 'https://get.$host/vlc-android/'; | ||||
|  | ||||
|   @override | ||||
|   Future<Map<String, String>?> getRequestHeaders( | ||||
|           {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||
|           bool forAPKDownload = false}) => | ||||
|       HTML().getRequestHeaders( | ||||
|           additionalSettings: additionalSettings, | ||||
|           forAPKDownload: forAPKDownload); | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     return 'https://$host'; | ||||
|   } | ||||
|  | ||||
|   Future<String?> getLatestVersion(String standardUrl) async { | ||||
|     Response res = await sourceRequest(dwUrlBase); | ||||
|     if (res.statusCode == 200) { | ||||
|       var dwLinks = parse(res.body) | ||||
|           .querySelectorAll('a') | ||||
|           .where((element) => element.attributes['href'] != 'last/') | ||||
|           .map((e) => e.attributes['href']?.split('/')[0]) | ||||
|           .toList(); | ||||
|       String? version = dwLinks.isNotEmpty ? dwLinks.last : null; | ||||
|       if (version == null) { | ||||
|         throw NoVersionError(); | ||||
|       } | ||||
|       return version; | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     Response res = await sourceRequest( | ||||
|         'https://www.videolan.org/vlc/download-android.html'); | ||||
|     Response res = await get( | ||||
|         Uri.parse('https://www.videolan.org/vlc/download-android.html')); | ||||
|     if (res.statusCode == 200) { | ||||
|       var dwUrlBase = 'get.videolan.org/vlc-android'; | ||||
|       var dwLinks = parse(res.body) | ||||
| @@ -37,20 +66,9 @@ class VLC extends AppSource { | ||||
|         throw NoVersionError(); | ||||
|       } | ||||
|       String? targetUrl = 'https://$dwUrlBase/$version/'; | ||||
|       Response res2 = await sourceRequest(targetUrl); | ||||
|       List<String> apkUrls = []; | ||||
|       if (res2.statusCode == 200) { | ||||
|         apkUrls = parse(res2.body) | ||||
|             .querySelectorAll('a') | ||||
|             .map((e) => e.attributes['href']?.split('/').last) | ||||
|             .where((h) => | ||||
|                 h != null && h.isNotEmpty && h.toLowerCase().endsWith('.apk')) | ||||
|             .map((e) => targetUrl + e!) | ||||
|             .toList(); | ||||
|       } else { | ||||
|         throw getObtainiumHttpError(res2); | ||||
|       } | ||||
|  | ||||
|       var apkUrls = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] | ||||
|           .map((e) => '${targetUrl}VLC-Android-$version-$e.apk') | ||||
|           .toList(); | ||||
|       return APKDetails( | ||||
|           version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC')); | ||||
|     } else { | ||||
| @@ -69,6 +87,20 @@ class VLC extends AppSource { | ||||
|         throw NoAPKError(); | ||||
|       } | ||||
|       return apkUrl; | ||||
|     } else if (res.statusCode == 500 && | ||||
|         res.body.toLowerCase().indexOf('mirror') > 0) { | ||||
|       var html = parse(res.body); | ||||
|       var err = ''; | ||||
|       html.body?.nodes.forEach((element) { | ||||
|         if (element.text != null) { | ||||
|           err += '${element.text}\n'; | ||||
|         } | ||||
|       }); | ||||
|       err = err.trim(); | ||||
|       if (err.isEmpty) { | ||||
|         err = tr('err'); | ||||
|       } | ||||
|       throw ObtainiumError(err); | ||||
|     } else { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem { | ||||
|   late int max; | ||||
|   late String? hint; | ||||
|   late bool password; | ||||
|   late TextInputType? textInputType; | ||||
|  | ||||
|   GeneratedFormTextField(String key, | ||||
|       {String label = 'Input', | ||||
| @@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem { | ||||
|       this.required = true, | ||||
|       this.max = 1, | ||||
|       this.hint, | ||||
|       this.password = false}) | ||||
|       this.password = false, | ||||
|       this.textInputType}) | ||||
|       : super(key, | ||||
|             label: label, | ||||
|             belowWidgets: belowWidgets, | ||||
| @@ -144,7 +146,8 @@ Color generateRandomLightColor() { | ||||
|   // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 | ||||
|   final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); | ||||
|   // Map RBG values from 0-1 to 0-255: | ||||
|   final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); | ||||
|   final List<int> rgbValues = | ||||
|       rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); | ||||
|   return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); | ||||
| } | ||||
|  | ||||
| @@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|         if (formItem is GeneratedFormTextField) { | ||||
|           final formFieldKey = GlobalKey<FormFieldState>(); | ||||
|           return TextFormField( | ||||
|             keyboardType: formItem.textInputType, | ||||
|             obscureText: formItem.password, | ||||
|             autocorrect: !formItem.password, | ||||
|             enableSuggestions: !formItem.password, | ||||
| @@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> { | ||||
|                     }) ?? | ||||
|                     [const SizedBox.shrink()], | ||||
|                 (values[widget.items[r][e].key] | ||||
|                 as Map<String, MapEntry<int, bool>>?) | ||||
|                     ?.values | ||||
|                     .where((e) => e.value) | ||||
|                     .length == 1 | ||||
|                                 as Map<String, MapEntry<int, bool>>?) | ||||
|                             ?.values | ||||
|                             .where((e) => e.value) | ||||
|                             .length == | ||||
|                         1 | ||||
|                     ? Padding( | ||||
|                     padding: const EdgeInsets.symmetric(horizontal: 4), | ||||
|                     child: IconButton( | ||||
|                       onPressed: () { | ||||
|                         setState(() { | ||||
|                           var temp = values[widget.items[r][e].key] | ||||
|                               as Map<String, MapEntry<int, bool>>; | ||||
|                           // get selected category str where bool is true | ||||
|                           final oldEntry = temp.entries.firstWhere((entry) => entry.value.value); | ||||
|                           // generate new color, ensure it is not the same | ||||
|                           int newColor = oldEntry.value.key; | ||||
|                           while(oldEntry.value.key == newColor) { | ||||
|                             newColor = generateRandomLightColor().value; | ||||
|                           } | ||||
|                           // Update entry with new color, remain selected | ||||
|                           temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value)); | ||||
|                           values[widget.items[r][e].key] = temp; | ||||
|                           someValueChanged(); | ||||
|                         }); | ||||
|                       }, | ||||
|                       icon: const Icon(Icons.format_color_fill_rounded), | ||||
|                       visualDensity: VisualDensity.compact, | ||||
|                       tooltip: tr('colour'), | ||||
|                     )) | ||||
|                         padding: const EdgeInsets.symmetric(horizontal: 4), | ||||
|                         child: IconButton( | ||||
|                           onPressed: () { | ||||
|                             setState(() { | ||||
|                               var temp = values[widget.items[r][e].key] | ||||
|                                   as Map<String, MapEntry<int, bool>>; | ||||
|                               // get selected category str where bool is true | ||||
|                               final oldEntry = temp.entries | ||||
|                                   .firstWhere((entry) => entry.value.value); | ||||
|                               // generate new color, ensure it is not the same | ||||
|                               int newColor = oldEntry.value.key; | ||||
|                               while (oldEntry.value.key == newColor) { | ||||
|                                 newColor = generateRandomLightColor().value; | ||||
|                               } | ||||
|                               // Update entry with new color, remain selected | ||||
|                               temp.update(oldEntry.key, | ||||
|                                   (old) => MapEntry(newColor, old.value)); | ||||
|                               values[widget.items[r][e].key] = temp; | ||||
|                               someValueChanged(); | ||||
|                             }); | ||||
|                           }, | ||||
|                           icon: const Icon(Icons.format_color_fill_rounded), | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                           tooltip: tr('colour'), | ||||
|                         )) | ||||
|                     : const SizedBox.shrink(), | ||||
|                 (values[widget.items[r][e].key] | ||||
|                                 as Map<String, MapEntry<int, bool>>?) | ||||
|   | ||||
							
								
								
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,10 +1,7 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:http/http.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'; | ||||
| @@ -22,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.23'; | ||||
| const String currentVersion = '0.14.15'; | ||||
| const String currentReleaseTag = | ||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||
|  | ||||
| @@ -41,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'; | ||||
| @@ -72,89 +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 ClientException) { | ||||
|         var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15; | ||||
|         logs.add( | ||||
|             plural('bgUpdateGotErrorRetryInMinutes', remainingMinutes, args: [ | ||||
|           e is ClientException | ||||
|               ? '${(e).message}, ${e.uri?.path}' | ||||
|               : 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 { | ||||
| @@ -212,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([ | ||||
| @@ -239,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; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -299,7 +220,9 @@ class _ObtainiumState extends State<Obtainium> { | ||||
|                   ? lightColorScheme | ||||
|                   : darkColorScheme, | ||||
|               fontFamily: 'Metropolis'), | ||||
|           home: const HomePage()); | ||||
|           home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{ | ||||
|             LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), | ||||
|           }, child: const HomePage())); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource { | ||||
|  | ||||
|   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( | ||||
|       String username, int page) async { | ||||
|     Response res = await get(Uri.parse( | ||||
|         'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page')); | ||||
|     Response res = await get( | ||||
|         Uri.parse( | ||||
|             'https://api.github.com/users/$username/starred?per_page=100&page=$page'), | ||||
|         headers: await GitHub().getRequestHeaders()); | ||||
|     if (res.statusCode == 200) { | ||||
|       Map<String, List<String>> urlsWithDescriptions = {}; | ||||
|       for (var e in (jsonDecode(res.body) as List<dynamic>)) { | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart'; | ||||
| import 'package:obtainium/pages/import_export.dart'; | ||||
| import 'package:obtainium/pages/settings.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/notifications_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|   Widget build(BuildContext context) { | ||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||
|     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); | ||||
|     NotificationsProvider notificationsProvider = | ||||
|         context.read<NotificationsProvider>(); | ||||
|  | ||||
|     bool doingSomething = gettingAppInfo || searching; | ||||
|  | ||||
| @@ -145,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                 userPickedTrackOnly))) { | ||||
|           var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; | ||||
|           app = await sourceProvider.getApp( | ||||
|               pickedSource!, userInput, additionalSettings, | ||||
|               pickedSource!, userInput.trim(), additionalSettings, | ||||
|               trackOnlyOverride: trackOnly, | ||||
|               overrideSource: pickedSourceOverride, | ||||
|               inferAppIdIfOptional: inferAppIdIfOptional); | ||||
| @@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); | ||||
|             // ignore: use_build_context_synchronously | ||||
|             var downloadedArtifact = await appsProvider.downloadApp( | ||||
|                 app, globalNavigatorKey.currentContext); | ||||
|                 app, globalNavigatorKey.currentContext, | ||||
|                 notificationsProvider: notificationsProvider); | ||||
|             DownloadedApk? downloadedFile; | ||||
|             DownloadedXApkDir? downloadedDir; | ||||
|             if (downloadedArtifact is DownloadedApk) { | ||||
| @@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             children: [ | ||||
|               const SizedBox( | ||||
|                 height: 48, | ||||
|               ), | ||||
|               Text( | ||||
|                 tr('supportedSourcesBelow'), | ||||
|                 tr('supportedSources'), | ||||
|                 style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|               ), | ||||
|               const SizedBox( | ||||
|                 height: 8, | ||||
|                 height: 16, | ||||
|               ), | ||||
|               ...sourceProvider.sources | ||||
|                   .map((e) => GestureDetector( | ||||
| @@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> { | ||||
|                                   : const SizedBox(); | ||||
|                             }, | ||||
|                             future: pickedSource?.getSourceNote()), | ||||
|                       const SizedBox( | ||||
|                         height: 16, | ||||
|                       SizedBox( | ||||
|                         height: pickedSource != null ? 16 : 96, | ||||
|                       ), | ||||
|                       if (pickedSource != null) | ||||
|                         getAdditionalOptsCol() | ||||
|                       else | ||||
|                         getSourcesListWidget(), | ||||
|                       const SizedBox( | ||||
|                         height: 8, | ||||
|                       if (pickedSource != null) getAdditionalOptsCol(), | ||||
|                       if (pickedSource == null) | ||||
|                         const Divider( | ||||
|                           height: 48, | ||||
|                         ), | ||||
|                       if (pickedSource == null) getSourcesListWidget(), | ||||
|                       SizedBox( | ||||
|                         height: pickedSource != null ? 8 : 2, | ||||
|                       ), | ||||
|                     ])), | ||||
|           ) | ||||
|   | ||||
| @@ -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, | ||||
|                     ) | ||||
| @@ -338,12 +338,18 @@ class _AppPageState extends State<AppPage> { | ||||
|                 try { | ||||
|                   HapticFeedback.heavyImpact(); | ||||
|                   var res = await appsProvider.downloadAndInstallLatestApps( | ||||
|                       app?.app.id != null ? [app!.app.id] : [], | ||||
|                       globalNavigatorKey.currentContext); | ||||
|                     app?.app.id != null ? [app!.app.id] : [], | ||||
|                     globalNavigatorKey.currentContext, | ||||
|                   ); | ||||
|                   if (app?.app.installedVersion != null && !trackOnly) { | ||||
|                     // ignore: use_build_context_synchronously | ||||
|                     showError(tr('appsUpdated'), context); | ||||
|                   } | ||||
|                   if (res.isNotEmpty && mounted) { | ||||
|                     Navigator.of(context).pop(); | ||||
|                   } | ||||
|                 } catch (e) { | ||||
|                   // ignore: use_build_context_synchronously | ||||
|                   showError(e, context); | ||||
|                 } | ||||
|               } | ||||
|   | ||||
| @@ -393,9 +393,9 @@ class AppsPageState extends State<AppsPage> { | ||||
|     } | ||||
|  | ||||
|     getAppIcon(int appIndex) { | ||||
|       return listedApps[appIndex].installedInfo != null | ||||
|       return listedApps[appIndex].icon != null | ||||
|           ? Image.memory( | ||||
|               listedApps[appIndex].installedInfo!.icon!, | ||||
|               listedApps[appIndex].icon!, | ||||
|               gaplessPlayback: true, | ||||
|             ) | ||||
|           : Row( | ||||
| @@ -448,33 +448,48 @@ class AppsPageState extends State<AppsPage> { | ||||
|               : const SizedBox.shrink(), | ||||
|           GestureDetector( | ||||
|               onTap: showChangesFn, | ||||
|               child: Column( | ||||
|                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                 children: [ | ||||
|                   Row(mainAxisSize: MainAxisSize.min, children: [ | ||||
|                     Container( | ||||
|                         constraints: BoxConstraints( | ||||
|                             maxWidth: MediaQuery.of(context).size.width / 4), | ||||
|                         child: Text(getVersionText(index), | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                             textAlign: TextAlign.end)), | ||||
|                   ]), | ||||
|                   Row( | ||||
|                     mainAxisSize: MainAxisSize.min, | ||||
|               child: Container( | ||||
|                   decoration: BoxDecoration( | ||||
|                       borderRadius: BorderRadius.circular(12), | ||||
|                       color: settingsProvider.highlightTouchTargets && | ||||
|                               showChangesFn != null | ||||
|                           ? (Theme.of(context).brightness == Brightness.light | ||||
|                                   ? Theme.of(context).primaryColor | ||||
|                                   : Theme.of(context).primaryColorLight) | ||||
|                               .withAlpha(20) | ||||
|                           : null), | ||||
|                   padding: settingsProvider.highlightTouchTargets | ||||
|                       ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0) | ||||
|                       : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0), | ||||
|                   child: Column( | ||||
|                     mainAxisAlignment: MainAxisAlignment.center, | ||||
|                     crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         getChangesButtonString(index, showChangesFn != null), | ||||
|                         style: TextStyle( | ||||
|                             fontStyle: FontStyle.italic, | ||||
|                             decoration: showChangesFn != null | ||||
|                                 ? TextDecoration.underline | ||||
|                                 : TextDecoration.none), | ||||
|                       ) | ||||
|                       Row(mainAxisSize: MainAxisSize.min, children: [ | ||||
|                         Container( | ||||
|                             constraints: BoxConstraints( | ||||
|                                 maxWidth: | ||||
|                                     MediaQuery.of(context).size.width / 4), | ||||
|                             child: Text(getVersionText(index), | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                                 textAlign: TextAlign.end)), | ||||
|                       ]), | ||||
|                       Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             getChangesButtonString( | ||||
|                                 index, showChangesFn != null), | ||||
|                             style: TextStyle( | ||||
|                                 fontStyle: FontStyle.italic, | ||||
|                                 decoration: showChangesFn != null | ||||
|                                     ? TextDecoration.underline | ||||
|                                     : TextDecoration.none), | ||||
|                           ) | ||||
|                         ], | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ], | ||||
|               )) | ||||
|                   ))) | ||||
|         ], | ||||
|       ); | ||||
|  | ||||
| @@ -643,15 +658,15 @@ class AppsPageState extends State<AppsPage> { | ||||
|                     label: tr('installX', args: [ | ||||
|                       plural('apps', newInstallIdsAllOrSelected.length) | ||||
|                     ]), | ||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty)); | ||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isEmpty)); | ||||
|               } | ||||
|               if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { | ||||
|                 formItems.add(GeneratedFormSwitch('trackonlies', | ||||
|                     label: tr('markXTrackOnlyAsUpdated', args: [ | ||||
|                       plural('apps', trackOnlyUpdateIdsAllOrSelected.length) | ||||
|                     ]), | ||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty || | ||||
|                         newInstallIdsAllOrSelected.isNotEmpty)); | ||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isEmpty && | ||||
|                         newInstallIdsAllOrSelected.isEmpty)); | ||||
|               } | ||||
|               showDialog<Map<String, dynamic>?>( | ||||
|                   context: context, | ||||
| @@ -684,11 +699,14 @@ class AppsPageState extends State<AppsPage> { | ||||
|                   } | ||||
|                   appsProvider | ||||
|                       .downloadAndInstallLatestApps( | ||||
|                           toInstall, globalNavigatorKey.currentContext, | ||||
|                           settingsProvider: settingsProvider) | ||||
|                           toInstall, globalNavigatorKey.currentContext) | ||||
|                       .catchError((e) { | ||||
|                     showError(e, context); | ||||
|                     return <String>[]; | ||||
|                   }).then((value) { | ||||
|                     if (shouldInstallUpdates) { | ||||
|                       showError(tr('appsUpdated'), context); | ||||
|                     } | ||||
|                   }); | ||||
|                 } | ||||
|               }); | ||||
|   | ||||
| @@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     SourceProvider sourceProvider = SourceProvider(); | ||||
|     var appsProvider = context.read<AppsProvider>(); | ||||
|     var settingsProvider = context.read<SettingsProvider>(); | ||||
|     var appsProvider = context.watch<AppsProvider>(); | ||||
|     var settingsProvider = context.watch<SettingsProvider>(); | ||||
|  | ||||
|     var outlineButtonStyle = ButtonStyle( | ||||
|       shape: MaterialStateProperty.all( | ||||
| @@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     runObtainiumExport() { | ||||
|     runObtainiumExport() async { | ||||
|       HapticFeedback.selectionClick(); | ||||
|       appsProvider.exportApps().then((String path) { | ||||
|         showError(tr('exportedTo', args: [path]), context); | ||||
|       appsProvider | ||||
|           .exportApps( | ||||
|               pickOnly: (await settingsProvider.getExportDir()) == null, | ||||
|               sp: settingsProvider) | ||||
|           .then((String? result) { | ||||
|         if (result != null) { | ||||
|           showError(tr('exportedTo', args: [result]), context); | ||||
|         } | ||||
|       }).catchError((e) { | ||||
|         showError(e, context); | ||||
|       }); | ||||
| @@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     children: [ | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           Expanded( | ||||
|                               child: TextButton( | ||||
|                                   style: outlineButtonStyle, | ||||
|                                   onPressed: appsProvider.apps.isEmpty || | ||||
|                                           importInProgress | ||||
|                                       ? null | ||||
|                                       : runObtainiumExport, | ||||
|                                   child: Text(tr('obtainiumExport')))), | ||||
|                           const SizedBox( | ||||
|                             width: 16, | ||||
|                           ), | ||||
|                           Expanded( | ||||
|                               child: TextButton( | ||||
|                                   style: outlineButtonStyle, | ||||
|                                   onPressed: importInProgress | ||||
|                                       ? null | ||||
|                                       : runObtainiumImport, | ||||
|                                   child: Text(tr('obtainiumImport')))) | ||||
|                         ], | ||||
|                       FutureBuilder( | ||||
|                         future: settingsProvider.getExportDir(), | ||||
|                         builder: (context, snapshot) { | ||||
|                           return Column( | ||||
|                             children: [ | ||||
|                               Row( | ||||
|                                 children: [ | ||||
|                                   Expanded( | ||||
|                                       child: TextButton( | ||||
|                                     style: outlineButtonStyle, | ||||
|                                     onPressed: appsProvider.apps.isEmpty || | ||||
|                                             importInProgress | ||||
|                                         ? null | ||||
|                                         : runObtainiumExport, | ||||
|                                     child: Text(tr(snapshot.data != null | ||||
|                                         ? 'obtainiumExport' | ||||
|                                         : 'pickExportDir')), | ||||
|                                   )), | ||||
|                                   const SizedBox( | ||||
|                                     width: 16, | ||||
|                                   ), | ||||
|                                   Expanded( | ||||
|                                       child: TextButton( | ||||
|                                           style: outlineButtonStyle, | ||||
|                                           onPressed: importInProgress | ||||
|                                               ? null | ||||
|                                               : runObtainiumImport, | ||||
|                                           child: Text(tr('obtainiumImport')))) | ||||
|                                 ], | ||||
|                               ), | ||||
|                               if (snapshot.data != null) | ||||
|                                 Column( | ||||
|                                   children: [ | ||||
|                                     const SizedBox(height: 16), | ||||
|                                     GeneratedForm( | ||||
|                                         items: [ | ||||
|                                           [ | ||||
|                                             GeneratedFormSwitch( | ||||
|                                               'autoExportOnChanges', | ||||
|                                               label: tr('autoExportOnChanges'), | ||||
|                                               defaultValue: settingsProvider | ||||
|                                                   .autoExportOnChanges, | ||||
|                                             ) | ||||
|                                           ] | ||||
|                                         ], | ||||
|                                         onValueChanges: | ||||
|                                             (value, valid, isBuilding) { | ||||
|                                           if (valid && !isBuilding) { | ||||
|                                             if (value['autoExportOnChanges'] != | ||||
|                                                 null) { | ||||
|                                               settingsProvider | ||||
|                                                   .autoExportOnChanges = value[ | ||||
|                                                       'autoExportOnChanges'] == | ||||
|                                                   true; | ||||
|                                             } | ||||
|                                           } | ||||
|                                         }), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                             ], | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                       if (importInProgress) | ||||
|                         const Column( | ||||
| @@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | ||||
|                               fontStyle: FontStyle.italic, fontSize: 12)), | ||||
|                       const SizedBox( | ||||
|                         height: 8, | ||||
|                       ) | ||||
|                       ), | ||||
|                     ], | ||||
|                   ))) | ||||
|         ])); | ||||
|   | ||||
| @@ -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'; | ||||
| @@ -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, | ||||
| @@ -412,6 +484,21 @@ class _SettingsPageState extends State<SettingsPage> { | ||||
|                                           }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height16, | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                               children: [ | ||||
|                                 Flexible( | ||||
|                                     child: Text(tr('highlightTouchTargets'))), | ||||
|                                 Switch( | ||||
|                                     value: | ||||
|                                         settingsProvider.highlightTouchTargets, | ||||
|                                     onChanged: (value) { | ||||
|                                       settingsProvider.highlightTouchTargets = | ||||
|                                           value; | ||||
|                                     }) | ||||
|                               ], | ||||
|                             ), | ||||
|                             height32, | ||||
|                             Text( | ||||
|                               tr('categories'), | ||||
| @@ -462,7 +549,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')) | ||||
|                         ], | ||||
|                       ), | ||||
|                   ]), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ) | ||||
|   | ||||
| @@ -4,22 +4,25 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'dart:math'; | ||||
| import 'package:http/http.dart' as http; | ||||
|  | ||||
| import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | ||||
| import 'package:android_intent_plus/flag.dart'; | ||||
| import 'package:android_package_installer/android_package_installer.dart'; | ||||
| import 'package:android_package_manager/android_package_manager.dart'; | ||||
| import 'package:connectivity_plus/connectivity_plus.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:installed_apps/app_info.dart'; | ||||
| import 'package:installed_apps/installed_apps.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/components/generated_form_modal.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/logs_provider.dart'; | ||||
| import 'package:obtainium/providers/notifications_provider.dart'; | ||||
| import 'package:obtainium/providers/settings_provider.dart'; | ||||
| import 'package:package_archive_info/package_archive_info.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| @@ -28,17 +31,21 @@ import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:android_intent_plus/android_intent.dart'; | ||||
| import 'package:flutter_archive/flutter_archive.dart'; | ||||
| import 'package:shared_storage/shared_storage.dart' as saf; | ||||
|  | ||||
| final pm = AndroidPackageManager(); | ||||
|  | ||||
| class AppInMemory { | ||||
|   late App app; | ||||
|   double? downloadProgress; | ||||
|   AppInfo? installedInfo; | ||||
|   PackageInfo? installedInfo; | ||||
|   Uint8List? icon; | ||||
|  | ||||
|   AppInMemory(this.app, this.downloadProgress, this.installedInfo); | ||||
|   AppInMemory(this.app, this.downloadProgress, this.installedInfo, this.icon); | ||||
|   AppInMemory deepCopy() => | ||||
|       AppInMemory(app.deepCopy(), downloadProgress, installedInfo); | ||||
|       AppInMemory(app.deepCopy(), downloadProgress, installedInfo, icon); | ||||
|  | ||||
|   String get name => app.overrideName ?? installedInfo?.name ?? app.finalName; | ||||
|   String get name => app.overrideName ?? app.finalName; | ||||
| } | ||||
|  | ||||
| class DownloadedApk { | ||||
| @@ -97,6 +104,41 @@ Set<String> findStandardFormatsForVersion(String version, bool strict) { | ||||
|   return results; | ||||
| } | ||||
|  | ||||
| moveStrToEnd(List<String> arr, String str, {String? strB}) { | ||||
|   String? temp; | ||||
|   arr.removeWhere((element) { | ||||
|     bool res = element == str || element == strB; | ||||
|     if (res) { | ||||
|       temp = element; | ||||
|     } | ||||
|     return res; | ||||
|   }); | ||||
|   if (temp != null) { | ||||
|     arr = [...arr, temp!]; | ||||
|   } | ||||
|   return arr; | ||||
| } | ||||
|  | ||||
| List<MapEntry<String, int>> moveStrToEndMapEntryWithCount( | ||||
|     List<MapEntry<String, int>> arr, MapEntry<String, int> str, | ||||
|     {MapEntry<String, int>? strB}) { | ||||
|   MapEntry<String, int>? temp; | ||||
|   arr.removeWhere((element) { | ||||
|     bool resA = element.key == str.key; | ||||
|     bool resB = element.key == strB?.key; | ||||
|     if (resA) { | ||||
|       temp = str; | ||||
|     } else if (resB) { | ||||
|       temp = strB; | ||||
|     } | ||||
|     return resA || resB; | ||||
|   }); | ||||
|   if (temp != null) { | ||||
|     arr = [...arr, temp!]; | ||||
|   } | ||||
|   return arr; | ||||
| } | ||||
|  | ||||
| class AppsProvider with ChangeNotifier { | ||||
|   // In memory App state (should always be kept in sync with local storage versions) | ||||
|   Map<String, AppInMemory> apps = {}; | ||||
| @@ -109,10 +151,11 @@ class AppsProvider with ChangeNotifier { | ||||
|   late Stream<FGBGType>? foregroundStream; | ||||
|   late StreamSubscription<FGBGType>? foregroundSubscription; | ||||
|   late Directory APKDir; | ||||
|   late SettingsProvider settingsProvider = SettingsProvider(); | ||||
|  | ||||
|   Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); | ||||
|  | ||||
|   AppsProvider() { | ||||
|   AppsProvider({isBg = false}) { | ||||
|     // Subscribe to changes in the app foreground status | ||||
|     foregroundStream = FGBGEvents.stream.asBroadcastStream(); | ||||
|     foregroundSubscription = foregroundStream?.listen((event) async { | ||||
| @@ -120,6 +163,7 @@ class AppsProvider with ChangeNotifier { | ||||
|       if (isForeground) await loadApps(); | ||||
|     }); | ||||
|     () async { | ||||
|       await settingsProvider.initializeSettings(); | ||||
|       var cacheDirs = await getExternalCacheDirectories(); | ||||
|       if (cacheDirs?.isNotEmpty ?? false) { | ||||
|         APKDir = cacheDirs!.first; | ||||
| @@ -130,17 +174,21 @@ class AppsProvider with ChangeNotifier { | ||||
|           APKDir.createSync(); | ||||
|         } | ||||
|       } | ||||
|       // Load Apps into memory (in background, this is done later instead of in the constructor) | ||||
|       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); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }(); | ||||
|   } | ||||
|  | ||||
| @@ -171,7 +219,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     if (headers != null) { | ||||
|       req.headers.addAll(headers); | ||||
|     } | ||||
|     var client = Client(); | ||||
|     var client = http.Client(); | ||||
|     StreamedResponse response = await client.send(req); | ||||
|     String ext = | ||||
|         response.headers['content-disposition']?.split('.').last ?? 'apk'; | ||||
| @@ -215,19 +263,19 @@ class AppsProvider with ChangeNotifier { | ||||
|     return downloadedFile; | ||||
|   } | ||||
|  | ||||
|   Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo, | ||||
|   Future<File> handleAPKIDChange(App app, PackageInfo newInfo, | ||||
|       File downloadedFile, String downloadUrl) async { | ||||
|     // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed | ||||
|     // The former case should be handled (give the App its real ID), the latter is a security issue | ||||
|     if (app.id != newInfo.packageName) { | ||||
|       var isTempId = SourceProvider().isTempId(app); | ||||
|       if (apps[app.id] != null && !isTempId && !app.allowIdChange) { | ||||
|         throw IDChangedError(newInfo.packageName); | ||||
|         throw IDChangedError(newInfo.packageName!); | ||||
|       } | ||||
|       var idChangeWasAllowed = app.allowIdChange; | ||||
|       app.allowIdChange = false; | ||||
|       var originalAppId = app.id; | ||||
|       app.id = newInfo.packageName; | ||||
|       app.id = newInfo.packageName!; | ||||
|       downloadedFile = downloadedFile.renameSync( | ||||
|           '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); | ||||
|       if (apps[originalAppId] != null) { | ||||
| @@ -238,9 +286,8 @@ class AppsProvider with ChangeNotifier { | ||||
|     return downloadedFile; | ||||
|   } | ||||
|  | ||||
|   Future<Object> downloadApp(App app, BuildContext? context) async { | ||||
|     NotificationsProvider? notificationsProvider = | ||||
|         context?.read<NotificationsProvider>(); | ||||
|   Future<Object> downloadApp(App app, BuildContext? context, | ||||
|       {NotificationsProvider? notificationsProvider}) async { | ||||
|     var notifId = DownloadNotification(app.finalName, 0).id; | ||||
|     if (apps[app.id] != null) { | ||||
|       apps[app.id]!.downloadProgress = 0; | ||||
| @@ -255,9 +302,11 @@ class AppsProvider with ChangeNotifier { | ||||
|       notificationsProvider?.cancel(notif.id); | ||||
|       int? prevProg; | ||||
|       var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; | ||||
|       var headers = await source.getRequestHeaders( | ||||
|           additionalSettings: app.additionalSettings, forAPKDownload: true); | ||||
|       var downloadedFile = await downloadFileWithRetry( | ||||
|           downloadUrl, fileNameNoExt, headers: source.requestHeaders, | ||||
|           (double? progress) { | ||||
|           downloadUrl, fileNameNoExt, | ||||
|           headers: headers, (double? progress) { | ||||
|         int? prog = progress?.ceil(); | ||||
|         if (apps[app.id] != null) { | ||||
|           apps[app.id]!.downloadProgress = progress; | ||||
| @@ -276,11 +325,12 @@ class AppsProvider with ChangeNotifier { | ||||
|         notif = DownloadNotification(app.finalName, -1); | ||||
|         notificationsProvider?.notify(notif); | ||||
|       } | ||||
|       PackageArchiveInfo? newInfo; | ||||
|       PackageInfo? newInfo; | ||||
|       var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk'); | ||||
|       Directory? xapkDir; | ||||
|       if (isAPK) { | ||||
|         newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); | ||||
|         newInfo = await pm.getPackageArchiveInfo( | ||||
|             archiveFilePath: downloadedFile.path); | ||||
|       } else { | ||||
|         // Assume XAPK | ||||
|         String xapkDirPath = '${downloadedFile.path}-dir'; | ||||
| @@ -290,10 +340,11 @@ class AppsProvider with ChangeNotifier { | ||||
|             .listSync() | ||||
|             .where((e) => e.path.toLowerCase().endsWith('.apk')) | ||||
|             .toList(); | ||||
|         newInfo = await PackageArchiveInfo.fromPath(apks.first.path); | ||||
|         newInfo = | ||||
|             await pm.getPackageArchiveInfo(archiveFilePath: apks.first.path); | ||||
|       } | ||||
|       downloadedFile = | ||||
|           await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); | ||||
|           await handleAPKIDChange(app, newInfo!, downloadedFile, downloadUrl); | ||||
|       // Delete older versions of the file if any | ||||
|       for (var file in downloadedFile.parent.listSync()) { | ||||
|         var fn = file.path.split('/').last; | ||||
| @@ -322,16 +373,42 @@ class AppsProvider with ChangeNotifier { | ||||
|       .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; | ||||
|     if (app.id == obtainiumId) { | ||||
|       return false; | ||||
|     } | ||||
|     if (!settingsProvider.enableBackgroundUpdates) { | ||||
|       return false; | ||||
|     } | ||||
|     if (app.additionalSettings['exemptFromBackgroundUpdates'] == true) { | ||||
|       return false; | ||||
|     } | ||||
|     if (app.apkUrls.length > 1) { | ||||
|       // Manual API selection means silent install is not possible | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     var osInfo = await DeviceInfoPlugin().androidInfo; | ||||
|     String? installerPackageName; | ||||
|     try { | ||||
|       installerPackageName = osInfo.version.sdkInt >= 30 | ||||
|           ? (await pm.getInstallSourceInfo(packageName: app.id)) | ||||
|               ?.installingPackageName | ||||
|           : (await pm.getInstallerPackageName(packageName: app.id)); | ||||
|     } catch (e) { | ||||
|       // Probably not installed - ignore | ||||
|     } | ||||
|     if (installerPackageName != obtainiumId) { | ||||
|       // If we did not install the app (or it isn't installed), silent install is not possible | ||||
|       return false; | ||||
|     } | ||||
|     int? targetSDK = | ||||
|         (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion; | ||||
|  | ||||
|     // The OS must also be new enough and the APK should target a new enough API | ||||
|     return osInfo.version.sdkInt >= 31 && | ||||
|         targetSDK != null && | ||||
|         targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) | ||||
|             (osInfo.version.sdkInt - 3); | ||||
|   } | ||||
|  | ||||
|   Future<void> waitForUserToReturnToForeground(BuildContext context) async { | ||||
| @@ -345,14 +422,8 @@ class AppsProvider with ChangeNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> canDowngradeApps() async { | ||||
|     try { | ||||
|       await InstalledApps.getAppInfo('com.berdik.letmedowngrade'); | ||||
|       return true; | ||||
|     } catch (e) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|   Future<bool> canDowngradeApps() async => | ||||
|       (await getInstalledInfo('com.berdik.letmedowngrade')) != null; | ||||
|  | ||||
|   Future<void> unzipFile(String filePath, String destinationPath) async { | ||||
|     await ZipFile.extractToDirectory( | ||||
| @@ -360,13 +431,13 @@ class AppsProvider with ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   Future<void> installXApkDir(DownloadedXApkDir dir, | ||||
|       {bool silent = false}) async { | ||||
|       {bool needsBGWorkaround = false}) async { | ||||
|     // We don't know which APKs in an XAPK are supported by the user's device | ||||
|     // So we try installing all of them and assume success if at least one installed | ||||
|     // If 0 APKs installed, throw the first install error encountered | ||||
|     try { | ||||
|       var somethingInstalled = false; | ||||
|       Object? firstError; | ||||
|       MultiAppMultiError errors = MultiAppMultiError(); | ||||
|       for (var file in dir.extracted | ||||
|           .listSync(recursive: true, followLinks: false) | ||||
|           .whereType<File>()) { | ||||
| @@ -374,11 +445,11 @@ class AppsProvider with ChangeNotifier { | ||||
|           try { | ||||
|             somethingInstalled = somethingInstalled || | ||||
|                 await installApk(DownloadedApk(dir.appId, file), | ||||
|                     silent: silent); | ||||
|                     needsBGWorkaround: needsBGWorkaround); | ||||
|           } catch (e) { | ||||
|             logs.add( | ||||
|                 'Could not install APK from XAPK \'${file.path}\': ${e.toString()}'); | ||||
|             firstError ??= e; | ||||
|             errors.add(dir.appId, e.toString()); | ||||
|           } | ||||
|         } else if (file.path.toLowerCase().endsWith('.obb')) { | ||||
|           await moveObbFile(file, dir.appId); | ||||
| @@ -386,28 +457,35 @@ class AppsProvider with ChangeNotifier { | ||||
|       } | ||||
|       if (somethingInstalled) { | ||||
|         dir.file.delete(recursive: true); | ||||
|       } else if (firstError != null) { | ||||
|         throw firstError; | ||||
|       } else if (errors.content.isNotEmpty) { | ||||
|         throw errors; | ||||
|       } | ||||
|     } finally { | ||||
|       dir.extracted.delete(recursive: true); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<bool> installApk(DownloadedApk file, {bool silent = false}) async { | ||||
|     // TODO: Use 'silent' when/if ever possible | ||||
|     var newInfo = await PackageArchiveInfo.fromPath(file.file.path); | ||||
|     AppInfo? appInfo; | ||||
|     try { | ||||
|       appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id); | ||||
|     } catch (e) { | ||||
|       // OK | ||||
|     } | ||||
|   Future<bool> installApk(DownloadedApk file, | ||||
|       {bool needsBGWorkaround = false}) async { | ||||
|     var newInfo = | ||||
|         await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); | ||||
|     PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); | ||||
|     if (appInfo != null && | ||||
|         int.parse(newInfo.buildNumber) < appInfo.versionCode! && | ||||
|         newInfo!.versionCode! < appInfo.versionCode! && | ||||
|         !(await canDowngradeApps())) { | ||||
|       throw DowngradeError(); | ||||
|     } | ||||
|     if (needsBGWorkaround) { | ||||
|       // The below 'await' will never return if we are in a background process | ||||
|       // To work around this, we should assume the install will be successful | ||||
|       // So we update the app's installed version first as we will never get to the later code | ||||
|       // We can't conditionally get rid of the 'await' as this causes install fails (BG process times out) - see #896 | ||||
|       // TODO: When fixed, update this function and the calls to it accordingly | ||||
|       apps[file.appId]!.app.installedVersion = | ||||
|           apps[file.appId]!.app.latestVersion; | ||||
|       await saveApps([apps[file.appId]!.app], | ||||
|           attemptToCorrectInstallStatus: false); | ||||
|     } | ||||
|     int? code = | ||||
|         await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); | ||||
|     bool installed = false; | ||||
| @@ -477,7 +555,6 @@ class AppsProvider with ChangeNotifier { | ||||
|         getHost(apkUrl.value) != getHost(app.url) && | ||||
|         context != null) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       var settingsProvider = context.read<SettingsProvider>(); | ||||
|       if (!(settingsProvider.hideAPKOriginWarning) && | ||||
|           // ignore: use_build_context_synchronously | ||||
|           await showDialog( | ||||
| @@ -500,7 +577,9 @@ class AppsProvider with ChangeNotifier { | ||||
|   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result | ||||
|   Future<List<String>> downloadAndInstallLatestApps( | ||||
|       List<String> appIds, BuildContext? context, | ||||
|       {SettingsProvider? settingsProvider}) async { | ||||
|       {NotificationsProvider? notificationsProvider}) async { | ||||
|     notificationsProvider = | ||||
|         notificationsProvider ?? context?.read<NotificationsProvider>(); | ||||
|     List<String> appsToInstall = []; | ||||
|     List<String> trackOnlyAppsToUpdate = []; | ||||
|     // For all specified Apps, filter out those for which: | ||||
| @@ -546,22 +625,15 @@ class AppsProvider with ChangeNotifier { | ||||
|     List<String> installedIds = []; | ||||
|  | ||||
|     // Move Obtainium to the end of the line (let all other apps update first) | ||||
|     String? temp; | ||||
|     appsToInstall.removeWhere((element) { | ||||
|       bool res = element == obtainiumId || element == obtainiumTempId; | ||||
|       if (res) { | ||||
|         temp = element; | ||||
|       } | ||||
|       return res; | ||||
|     }); | ||||
|     if (temp != null) { | ||||
|       appsToInstall = [...appsToInstall, temp!]; | ||||
|     } | ||||
|     appsToInstall = | ||||
|         moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId); | ||||
|  | ||||
|     for (var id in appsToInstall) { | ||||
|       try { | ||||
|         // 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) { | ||||
| @@ -569,11 +641,9 @@ class AppsProvider with ChangeNotifier { | ||||
|         } else { | ||||
|           downloadedDir = downloadedArtifact as DownloadedXApkDir; | ||||
|         } | ||||
|         bool willBeSilent = await canInstallSilently( | ||||
|             apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app); | ||||
|         willBeSilent = false; // TODO: Remove this when silent updates work | ||||
|         if (!(await settingsProvider?.getInstallPermission(enforce: false) ?? | ||||
|             true)) { | ||||
|         var appId = downloadedFile?.appId ?? downloadedDir!.appId; | ||||
|         bool willBeSilent = await canInstallSilently(apps[appId]!.app); | ||||
|         if (!(await settingsProvider.getInstallPermission(enforce: false))) { | ||||
|           throw ObtainiumError(tr('cancelled')); | ||||
|         } | ||||
|         if (!willBeSilent && context != null) { | ||||
| @@ -584,9 +654,22 @@ class AppsProvider with ChangeNotifier { | ||||
|         notifyListeners(); | ||||
|         try { | ||||
|           if (downloadedFile != null) { | ||||
|             await installApk(downloadedFile, silent: willBeSilent); | ||||
|             if (willBeSilent && context == null) { | ||||
|               installApk(downloadedFile, needsBGWorkaround: true); | ||||
|             } else { | ||||
|               await installApk(downloadedFile); | ||||
|             } | ||||
|           } else { | ||||
|             await installXApkDir(downloadedDir!, silent: willBeSilent); | ||||
|             if (willBeSilent && context == null) { | ||||
|               installXApkDir(downloadedDir!, needsBGWorkaround: true); | ||||
|             } else { | ||||
|               await installXApkDir(downloadedDir!); | ||||
|             } | ||||
|           } | ||||
|           if (willBeSilent && context == null) { | ||||
|             notificationsProvider?.notify(SilentUpdateAttemptNotification( | ||||
|                 [apps[appId]!.app], | ||||
|                 id: appId.hashCode)); | ||||
|           } | ||||
|         } finally { | ||||
|           apps[id]?.downloadProgress = null; | ||||
| @@ -602,41 +685,29 @@ class AppsProvider with ChangeNotifier { | ||||
|       throw errors; | ||||
|     } | ||||
|  | ||||
|     NotificationsProvider().cancel(UpdateNotification([]).id); | ||||
|  | ||||
|     return installedIds; | ||||
|   } | ||||
|  | ||||
|   Future<Directory> getAppsDir() async { | ||||
|     Directory appsDir = Directory( | ||||
|         '${(await getExternalStorageDirectory())?.path as String}/app_data'); | ||||
|     Directory appsDir = | ||||
|         Directory('${(await getExternalStorageDirectory())!.path}/app_data'); | ||||
|     if (!appsDir.existsSync()) { | ||||
|       appsDir.createSync(); | ||||
|     } | ||||
|     return appsDir; | ||||
|   } | ||||
|  | ||||
|   Future<AppInfo?> getInstalledInfo(String? packageName) async { | ||||
|   Future<PackageInfo?> getInstalledInfo(String? packageName) async { | ||||
|     if (packageName != null) { | ||||
|       try { | ||||
|         return await InstalledApps.getAppInfo(packageName); | ||||
|         return await pm.getPackageInfo(packageName: packageName); | ||||
|       } catch (e) { | ||||
|         // OK | ||||
|         print(e); // OK | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<bool> doesInstalledAppsPluginWork() async { | ||||
|     bool res = false; | ||||
|     try { | ||||
|       res = (await InstalledApps.getAppInfo(obtainiumId)).versionName != null; | ||||
|     } catch (e) { | ||||
|       // | ||||
|     } | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   bool isVersionDetectionPossible(AppInMemory? app) { | ||||
|     return app?.app.additionalSettings['trackOnly'] != true && | ||||
|         app?.app.additionalSettings['versionDetection'] != | ||||
| @@ -650,7 +721,8 @@ class AppsProvider with ChangeNotifier { | ||||
|  | ||||
|   // Given an App and it's on-device info... | ||||
|   // Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version | ||||
|   App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) { | ||||
|   App? getCorrectedInstallStatusAppIfPossible( | ||||
|       App app, PackageInfo? installedInfo) { | ||||
|     var modded = false; | ||||
|     var trackOnly = app.additionalSettings['trackOnly'] == true; | ||||
|     var noVersionDetection = app.additionalSettings['versionDetection'] != | ||||
| @@ -696,22 +768,12 @@ class AppsProvider with ChangeNotifier { | ||||
|     if (installedInfo != null && | ||||
|         app.additionalSettings['versionDetection'] == | ||||
|             'standardVersionDetection' && | ||||
|         !isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) { | ||||
|         !isVersionDetectionPossible( | ||||
|             AppInMemory(app, null, installedInfo, null))) { | ||||
|       app.additionalSettings['versionDetection'] = 'noVersionDetection'; | ||||
|       logs.add('Could not reconcile version formats for: ${app.id}'); | ||||
|       modded = true; | ||||
|     } | ||||
|     // if (app.installedVersion != null && | ||||
|     //     app.additionalSettings['versionDetection'] == | ||||
|     //         'standardVersionDetection') { | ||||
|     //   var correctedInstalledVersion = | ||||
|     //       reconcileVersionDifferences(app.installedVersion!, app.latestVersion); | ||||
|     //   if (correctedInstalledVersion == null) { | ||||
|     //     app.additionalSettings['versionDetection'] = 'noVersionDetection'; | ||||
|     //     logs.add('Could not reconcile version formats for: ${app.id}'); | ||||
|     //     modded = true; | ||||
|     //   } | ||||
|     // } | ||||
|  | ||||
|     return modded ? app : null; | ||||
|   } | ||||
| @@ -750,7 +812,7 @@ class AppsProvider with ChangeNotifier { | ||||
|         : false; | ||||
|   } | ||||
|  | ||||
|   Future<void> loadApps() async { | ||||
|   Future<void> loadApps({String? singleId}) async { | ||||
|     while (loadingApps) { | ||||
|       await Future.delayed(const Duration(microseconds: 1)); | ||||
|     } | ||||
| @@ -761,6 +823,10 @@ class AppsProvider with ChangeNotifier { | ||||
|     List<App?> newApps = (await getAppsDir()) // Parse Apps from JSON | ||||
|         .listSync() | ||||
|         .where((item) => item.path.toLowerCase().endsWith('.json')) | ||||
|         .where((item) => | ||||
|             singleId == null || | ||||
|             item.path.split('/').last.toLowerCase() == | ||||
|                 '${singleId.toLowerCase()}.json') | ||||
|         .map((e) { | ||||
|       try { | ||||
|         return App.fromJson(jsonDecode(File(e.path).readAsStringSync())); | ||||
| @@ -780,9 +846,9 @@ class AppsProvider with ChangeNotifier { | ||||
|           sp.getSource(app.url, overrideSource: app.overrideSource); | ||||
|           apps.update( | ||||
|               app.id, | ||||
|               (value) => | ||||
|                   AppInMemory(app, value.downloadProgress, value.installedInfo), | ||||
|               ifAbsent: () => AppInMemory(app, null, null)); | ||||
|               (value) => AppInMemory( | ||||
|                   app, value.downloadProgress, value.installedInfo, value.icon), | ||||
|               ifAbsent: () => AppInMemory(app, null, null, null)); | ||||
|         } catch (e) { | ||||
|           errors.add([app.id, app.finalName, e.toString()]); | ||||
|         } | ||||
| @@ -795,34 +861,37 @@ 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) { | ||||
|         if (settingsProvider.removeOnExternalUninstall) { | ||||
|           await removeApps(removedAppIds); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -834,12 +903,12 @@ class AppsProvider with ChangeNotifier { | ||||
|   Future<void> saveApps(List<App> apps, | ||||
|       {bool attemptToCorrectInstallStatus = true, | ||||
|       bool onlyIfExists = true}) async { | ||||
|     attemptToCorrectInstallStatus = | ||||
|         attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork()); | ||||
|     attemptToCorrectInstallStatus = attemptToCorrectInstallStatus; | ||||
|     for (var a in apps) { | ||||
|       var app = a.deepCopy(); | ||||
|       AppInfo? info = await getInstalledInfo(app.id); | ||||
|       app.name = info?.name ?? app.name; | ||||
|       PackageInfo? info = await getInstalledInfo(app.id); | ||||
|       var icon = await info?.applicationInfo?.getAppIcon(); | ||||
|       app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name; | ||||
|       if (attemptToCorrectInstallStatus) { | ||||
|         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; | ||||
|       } | ||||
| @@ -848,9 +917,10 @@ class AppsProvider with ChangeNotifier { | ||||
|             .writeAsStringSync(jsonEncode(app.toJson())); | ||||
|       } | ||||
|       try { | ||||
|         this.apps.update( | ||||
|             app.id, (value) => AppInMemory(app, value.downloadProgress, info), | ||||
|             ifAbsent: onlyIfExists ? null : () => AppInMemory(app, null, info)); | ||||
|         this.apps.update(app.id, | ||||
|             (value) => AppInMemory(app, value.downloadProgress, info, icon), | ||||
|             ifAbsent: | ||||
|                 onlyIfExists ? null : () => AppInMemory(app, null, info, icon)); | ||||
|       } catch (e) { | ||||
|         if (e is! ArgumentError || e.name != 'key') { | ||||
|           rethrow; | ||||
| @@ -858,6 +928,7 @@ class AppsProvider with ChangeNotifier { | ||||
|       } | ||||
|     } | ||||
|     notifyListeners(); | ||||
|     exportApps(isAuto: true); | ||||
|   } | ||||
|  | ||||
|   Future<void> removeApps(List<String> appIds) async { | ||||
| @@ -879,6 +950,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     } | ||||
|     if (appIds.isNotEmpty) { | ||||
|       notifyListeners(); | ||||
|       exportApps(isAuto: true); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -965,6 +1037,22 @@ class AppsProvider with ChangeNotifier { | ||||
|     return newApp.latestVersion != currentApp.latestVersion ? newApp : null; | ||||
|   } | ||||
|  | ||||
|   List<String> getAppsSortedByUpdateCheckTime( | ||||
|       {DateTime? ignoreAppsCheckedAfter}) { | ||||
|     List<String> appIds = apps.values | ||||
|         .where((app) => | ||||
|             app.app.lastUpdateCheck == null || | ||||
|             ignoreAppsCheckedAfter == null || | ||||
|             app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter)) | ||||
|         .map((e) => e.app.id) | ||||
|         .toList(); | ||||
|     appIds.sort((a, b) => | ||||
|         (apps[a]!.app.lastUpdateCheck ?? DateTime.fromMicrosecondsSinceEpoch(0)) | ||||
|             .compareTo(apps[b]!.app.lastUpdateCheck ?? | ||||
|                 DateTime.fromMicrosecondsSinceEpoch(0))); | ||||
|     return appIds; | ||||
|   } | ||||
|  | ||||
|   Future<List<App>> checkUpdates( | ||||
|       {DateTime? ignoreAppsCheckedAfter, | ||||
|       bool throwErrorsForRetry = false}) async { | ||||
| @@ -973,17 +1061,8 @@ class AppsProvider with ChangeNotifier { | ||||
|     if (!gettingUpdates) { | ||||
|       gettingUpdates = true; | ||||
|       try { | ||||
|         List<String> appIds = apps.values | ||||
|             .where((app) => | ||||
|                 app.app.lastUpdateCheck == null || | ||||
|                 ignoreAppsCheckedAfter == null || | ||||
|                 app.app.lastUpdateCheck!.isBefore(ignoreAppsCheckedAfter)) | ||||
|             .map((e) => e.app.id) | ||||
|             .toList(); | ||||
|         appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ?? | ||||
|                 DateTime.fromMicrosecondsSinceEpoch(0)) | ||||
|             .compareTo(apps[b]!.app.lastUpdateCheck ?? | ||||
|                 DateTime.fromMicrosecondsSinceEpoch(0))); | ||||
|         List<String> appIds = getAppsSortedByUpdateCheckTime( | ||||
|             ignoreAppsCheckedAfter: ignoreAppsCheckedAfter); | ||||
|         for (int i = 0; i < appIds.length; i++) { | ||||
|           App? newApp; | ||||
|           try { | ||||
| @@ -1028,32 +1107,52 @@ class AppsProvider with ChangeNotifier { | ||||
|     return updateAppIds; | ||||
|   } | ||||
|  | ||||
|   Future<String> exportApps() async { | ||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         await Permission.storage.request(); | ||||
|   Future<String?> exportApps( | ||||
|       {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async { | ||||
|     SettingsProvider settingsProvider = sp ?? this.settingsProvider; | ||||
|     var exportDir = await settingsProvider.getExportDir(); | ||||
|     if (isAuto) { | ||||
|       if (settingsProvider.autoExportOnChanges != true) { | ||||
|         return null; | ||||
|       } | ||||
|       if (await Permission.storage.isDenied) { | ||||
|         throw ObtainiumError(tr('storagePermissionDenied')); | ||||
|       if (exportDir == null) { | ||||
|         logs.add('Skipping auto-export as dir is not set.'); | ||||
|         return null; | ||||
|       } | ||||
|       logs.add('Started auto-export.'); | ||||
|       var files = await saf | ||||
|           .listFiles(exportDir, columns: [saf.DocumentFileColumn.id]) | ||||
|           .where((f) => f.uri.pathSegments.last.endsWith('-auto.json')) | ||||
|           .toList(); | ||||
|       if (files.isNotEmpty) { | ||||
|         for (var f in files) { | ||||
|           saf.delete(f.uri); | ||||
|         } | ||||
|         logs.add('Previous auto-export deleted.'); | ||||
|       } | ||||
|     } | ||||
|     Directory? exportDir = Directory('/storage/emulated/0/Download'); | ||||
|     String path = 'Downloads'; // TODO: See if hardcoding this can be avoided | ||||
|     var downloadsAccessible = false; | ||||
|     try { | ||||
|       downloadsAccessible = exportDir.existsSync(); | ||||
|     } catch (e) { | ||||
|       logs.add('Error accessing Downloads (will use fallback): $e'); | ||||
|     if (exportDir == null || pickOnly) { | ||||
|       await settingsProvider.pickExportDir(); | ||||
|       exportDir = await settingsProvider.getExportDir(); | ||||
|     } | ||||
|     if (!downloadsAccessible) { | ||||
|       exportDir = await getExternalStorageDirectory(); | ||||
|       path = exportDir!.path; | ||||
|     if (exportDir == null) { | ||||
|       return null; | ||||
|     } | ||||
|     File export = File( | ||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); | ||||
|     export.writeAsStringSync( | ||||
|         jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); | ||||
|     return path; | ||||
|     String? returnPath; | ||||
|     if (!pickOnly) { | ||||
|       var result = await saf.createFile(exportDir, | ||||
|           displayName: | ||||
|               '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', | ||||
|           mimeType: 'application/json', | ||||
|           bytes: Uint8List.fromList(utf8.encode( | ||||
|               jsonEncode(apps.values.map((e) => e.app.toJson()).toList())))); | ||||
|       if (result == null) { | ||||
|         throw ObtainiumError(tr('unexpectedError')); | ||||
|       } | ||||
|       returnPath = | ||||
|           exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/'); | ||||
|     } | ||||
|     return returnPath; | ||||
|   } | ||||
|  | ||||
|   Future<int> importApps(String appsJSON) async { | ||||
| @@ -1200,3 +1299,217 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Background updater function | ||||
| /// | ||||
| /// @param List<String>? toCheck: The appIds to check for updates (default to all apps sorted by last update check time) | ||||
| /// | ||||
| /// @param List<String>? toInstall: The appIds to attempt to update (defaults to an empty array) | ||||
| /// | ||||
| /// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0) | ||||
| /// | ||||
| /// When toCheck is empty, the function is in "install mode" (else it is in "update mode"). | ||||
| /// In update mode, all apps in toCheck are checked for updates. | ||||
| /// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified. | ||||
| /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. | ||||
| /// | ||||
| /// Once all update checks are complete, the function is called again in install mode. | ||||
| /// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown). | ||||
| /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. | ||||
| /// | ||||
| /// In either mode, if the function fails after the maximum number of tries, the user is notified. | ||||
| @pragma('vm:entry-point') | ||||
| Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | ||||
|   WidgetsFlutterBinding.ensureInitialized(); | ||||
|   await EasyLocalization.ensureInitialized(); | ||||
|   await AndroidAlarmManager.initialize(); | ||||
|   await loadTranslations(); | ||||
|  | ||||
|   LogsProvider logs = LogsProvider(); | ||||
|   NotificationsProvider notificationsProvider = NotificationsProvider(); | ||||
|   AppsProvider appsProvider = AppsProvider(isBg: true); | ||||
|   await appsProvider.loadApps(); | ||||
|  | ||||
|   int maxAttempts = 4; | ||||
|  | ||||
|   params ??= {}; | ||||
|   if (params['toCheck'] == null) { | ||||
|     appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); | ||||
|   } | ||||
|   List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[ | ||||
|     ...(params['toCheck'] | ||||
|             ?.map((entry) => MapEntry<String, int>( | ||||
|                 entry['key'] as String, entry['value'] as int)) | ||||
|             .toList() ?? | ||||
|         appsProvider | ||||
|             .getAppsSortedByUpdateCheckTime() | ||||
|             .map((e) => MapEntry(e, 0))) | ||||
|   ]; | ||||
|   List<MapEntry<String, int>> toInstall = <MapEntry<String, int>>[ | ||||
|     ...(params['toInstall'] | ||||
|             ?.map((entry) => MapEntry<String, int>( | ||||
|                 entry['key'] as String, entry['value'] as int)) | ||||
|             .toList() ?? | ||||
|         (<List<MapEntry<String, int>>>[])) | ||||
|   ]; | ||||
|  | ||||
|   bool installMode = toCheck.isEmpty && | ||||
|       toInstall.isNotEmpty; // Task is either in update mode or install mode | ||||
|  | ||||
|   logs.add( | ||||
|       'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); | ||||
|  | ||||
|   if (!installMode) { | ||||
|     // If in update mode... | ||||
|     var didCompleteChecking = false; | ||||
|     CheckingUpdatesNotification? notif; | ||||
|     var networkRestricted = false; | ||||
|     if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { | ||||
|       var netResult = await (Connectivity().checkConnectivity()); | ||||
|       networkRestricted = (netResult != ConnectivityResult.wifi) && | ||||
|           (netResult != ConnectivityResult.ethernet); | ||||
|     } | ||||
|     // Loop through all updates and check each | ||||
|     List<App> toNotify = []; | ||||
|     try { | ||||
|       for (int i = 0; i < toCheck.length; i++) { | ||||
|         var appId = toCheck[i].key; | ||||
|         var attemptCount = toCheck[i].value + 1; | ||||
|         AppInMemory? app = appsProvider.apps[appId]; | ||||
|         if (app?.app.installedVersion != null) { | ||||
|           try { | ||||
|             notificationsProvider.notify( | ||||
|                 notif = CheckingUpdatesNotification(app?.name ?? appId), | ||||
|                 cancelExisting: true); | ||||
|             App? newApp = await appsProvider.checkUpdate(appId); | ||||
|             if (newApp != null) { | ||||
|               if (networkRestricted || | ||||
|                   !(await appsProvider.canInstallSilently(app!.app))) { | ||||
|                 toNotify.add(newApp); | ||||
|               } else { | ||||
|                 toInstall.add(MapEntry(appId, 0)); | ||||
|               } | ||||
|             } | ||||
|             if (i == (toCheck.length - 1)) { | ||||
|               didCompleteChecking = true; | ||||
|             } | ||||
|           } catch (e) { | ||||
|             // If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue checking shortly | ||||
|             logs.add( | ||||
|                 'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.'); | ||||
|             if (attemptCount < maxAttempts) { | ||||
|               var remainingSeconds = e is RateLimitError | ||||
|                   ? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60)) | ||||
|                   : e is ClientException | ||||
|                       ? (15 * 60) | ||||
|                       : pow(attemptCount, 2).toInt(); | ||||
|               logs.add( | ||||
|                   'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); | ||||
|               var remainingToCheck = moveStrToEndMapEntryWithCount( | ||||
|                   toCheck.sublist(i), MapEntry(appId, attemptCount)); | ||||
|               AndroidAlarmManager.oneShot(Duration(seconds: remainingSeconds), | ||||
|                   taskId + 1, bgUpdateCheck, | ||||
|                   params: { | ||||
|                     'toCheck': remainingToCheck | ||||
|                         .map( | ||||
|                             (entry) => {'key': entry.key, 'value': entry.value}) | ||||
|                         .toList(), | ||||
|                     'toInstall': toInstall | ||||
|                         .map( | ||||
|                             (entry) => {'key': entry.key, 'value': entry.value}) | ||||
|                         .toList(), | ||||
|                   }); | ||||
|               break; | ||||
|             } else { | ||||
|               // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) | ||||
|               toCheck.removeAt(i); | ||||
|               i--; | ||||
|               notificationsProvider | ||||
|                   .notify(ErrorCheckingUpdatesNotification(e.toString())); | ||||
|             } | ||||
|           } finally { | ||||
|             if (notif != null) { | ||||
|               notificationsProvider.cancel(notif.id); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } finally { | ||||
|       if (toNotify.isNotEmpty) { | ||||
|         notificationsProvider.notify(UpdateNotification(toNotify)); | ||||
|       } | ||||
|     } | ||||
|     // If you're done checking and found some silently installable updates, schedule another task which will run in install mode | ||||
|     if (didCompleteChecking && toInstall.isNotEmpty) { | ||||
|       logs.add( | ||||
|           'BG update task $taskId: Done. Scheduling install task to run immediately.'); | ||||
|       AndroidAlarmManager.oneShot( | ||||
|           const Duration(minutes: 0), taskId + 1, bgUpdateCheck, | ||||
|           params: { | ||||
|             'toCheck': [], | ||||
|             'toInstall': toInstall | ||||
|                 .map((entry) => {'key': entry.key, 'value': entry.value}) | ||||
|                 .toList() | ||||
|           }); | ||||
|     } else if (didCompleteChecking) { | ||||
|       logs.add('BG install task $taskId: Done.'); | ||||
|     } | ||||
|   } else { | ||||
|     // If in install mode... | ||||
|     var didCompleteInstalling = false; | ||||
|     var tempObtArr = toInstall.where((element) => element.key == obtainiumId); | ||||
|     if (tempObtArr.isNotEmpty) { | ||||
|       // Move obtainium to the end of the list as it must always install last | ||||
|       var obt = tempObtArr.first; | ||||
|       toInstall = moveStrToEndMapEntryWithCount(toInstall, obt); | ||||
|     } | ||||
|     // Loop through all updates and install each | ||||
|     for (var i = 0; i < toInstall.length; i++) { | ||||
|       var appId = toInstall[i].key; | ||||
|       var retryCount = toInstall[i].value; | ||||
|       try { | ||||
|         logs.add( | ||||
|             'BG install task $taskId: Attempting to update $appId in the background.'); | ||||
|         await appsProvider.downloadAndInstallLatestApps([appId], null, | ||||
|             notificationsProvider: notificationsProvider); | ||||
|         await Future.delayed(const Duration( | ||||
|             seconds: | ||||
|                 5)); // Just in case task ending causes install fail (not clear) | ||||
|         if (i == (toCheck.length - 1)) { | ||||
|           didCompleteInstalling = true; | ||||
|         } | ||||
|       } catch (e) { | ||||
|         // If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly | ||||
|         logs.add( | ||||
|             'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.'); | ||||
|         if (retryCount < maxAttempts) { | ||||
|           var remainingSeconds = retryCount; | ||||
|           logs.add( | ||||
|               'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); | ||||
|           var remainingToInstall = moveStrToEndMapEntryWithCount( | ||||
|               toInstall.sublist(i), MapEntry(appId, retryCount + 1)); | ||||
|           AndroidAlarmManager.oneShot( | ||||
|               Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, | ||||
|               params: { | ||||
|                 'toCheck': toCheck | ||||
|                     .map((entry) => {'key': entry.key, 'value': entry.value}) | ||||
|                     .toList(), | ||||
|                 'toInstall': remainingToInstall | ||||
|                     .map((entry) => {'key': entry.key, 'value': entry.value}) | ||||
|                     .toList(), | ||||
|               }); | ||||
|           break; | ||||
|         } else { | ||||
|           // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) | ||||
|           toInstall.removeAt(i); | ||||
|           i--; | ||||
|           notificationsProvider | ||||
|               .notify(ErrorCheckingUpdatesNotification(e.toString())); | ||||
|         } | ||||
|       } | ||||
|       if (didCompleteInstalling) { | ||||
|         logs.add('BG install task $taskId: Done.'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -22,9 +22,9 @@ class ObtainiumNotification { | ||||
| } | ||||
|  | ||||
| class UpdateNotification extends ObtainiumNotification { | ||||
|   UpdateNotification(List<App> updates) | ||||
|   UpdateNotification(List<App> updates, {int? id}) | ||||
|       : super( | ||||
|             2, | ||||
|             id ?? 2, | ||||
|             tr('updatesAvailable'), | ||||
|             '', | ||||
|             'UPDATES_AVAILABLE', | ||||
| @@ -41,8 +41,8 @@ class UpdateNotification extends ObtainiumNotification { | ||||
| } | ||||
|  | ||||
| class SilentUpdateNotification extends ObtainiumNotification { | ||||
|   SilentUpdateNotification(List<App> updates) | ||||
|       : super(3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'), | ||||
|   SilentUpdateNotification(List<App> updates, {int? id}) | ||||
|       : super(id ?? 3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'), | ||||
|             tr('appsUpdatedNotifDescription'), Importance.defaultImportance) { | ||||
|     message = updates.length == 1 | ||||
|         ? tr('xWasUpdatedToY', | ||||
| @@ -52,10 +52,28 @@ class SilentUpdateNotification extends ObtainiumNotification { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ErrorCheckingUpdatesNotification extends ObtainiumNotification { | ||||
|   ErrorCheckingUpdatesNotification(String error) | ||||
| class SilentUpdateAttemptNotification extends ObtainiumNotification { | ||||
|   SilentUpdateAttemptNotification(List<App> updates, {int? id}) | ||||
|       : super( | ||||
|             5, | ||||
|             id ?? 3, | ||||
|             tr('appsPossiblyUpdated'), | ||||
|             '', | ||||
|             'APPS_POSSIBLY_UPDATED', | ||||
|             tr('appsPossiblyUpdated'), | ||||
|             tr('appsPossiblyUpdatedNotifDescription'), | ||||
|             Importance.defaultImportance) { | ||||
|     message = updates.length == 1 | ||||
|         ? tr('xWasPossiblyUpdatedToY', | ||||
|             args: [updates[0].finalName, updates[0].latestVersion]) | ||||
|         : plural('xAndNMoreUpdatesPossiblyInstalled', updates.length - 1, | ||||
|             args: [updates[0].finalName, (updates.length - 1).toString()]); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ErrorCheckingUpdatesNotification extends ObtainiumNotification { | ||||
|   ErrorCheckingUpdatesNotification(String error, {int? id}) | ||||
|       : super( | ||||
|             id ?? 5, | ||||
|             tr('errorCheckingUpdates'), | ||||
|             error, | ||||
|             'BG_UPDATE_CHECK_ERROR', | ||||
| @@ -99,14 +117,17 @@ final completeInstallationNotification = ObtainiumNotification( | ||||
|     tr('completeAppInstallationNotifDescription'), | ||||
|     Importance.max); | ||||
|  | ||||
| final checkingUpdatesNotification = ObtainiumNotification( | ||||
|     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 = | ||||
|   | ||||
| @@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/main.dart'; | ||||
| import 'package:obtainium/providers/apps_provider.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:permission_handler/permission_handler.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:shared_storage/shared_storage.dart' as saf; | ||||
|  | ||||
| String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; | ||||
| String obtainiumId = 'dev.imranr.obtainium'; | ||||
| @@ -35,6 +37,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] | ||||
|  | ||||
| class SettingsProvider with ChangeNotifier { | ||||
|   SharedPreferences? prefs; | ||||
|   String? defaultAppDir; | ||||
|   bool justStarted = true; | ||||
|  | ||||
|   String sourceUrl = 'https://github.com/ImranR98/Obtainium'; | ||||
| @@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier { | ||||
|   // Not done in constructor as we want to be able to await it | ||||
|   Future<void> initializeSettings() async { | ||||
|     prefs = await SharedPreferences.getInstance(); | ||||
|     defaultAppDir = (await getExternalStorageDirectory())!.path; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
| @@ -309,4 +313,97 @@ class SettingsProvider with ChangeNotifier { | ||||
|     prefs?.setBool('reversePageTransitions', show); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get enableBackgroundUpdates { | ||||
|     return prefs?.getBool('enableBackgroundUpdates') ?? true; | ||||
|   } | ||||
|  | ||||
|   set enableBackgroundUpdates(bool val) { | ||||
|     prefs?.setBool('enableBackgroundUpdates', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get bgUpdatesOnWiFiOnly { | ||||
|     return prefs?.getBool('bgUpdatesOnWiFiOnly') ?? false; | ||||
|   } | ||||
|  | ||||
|   set bgUpdatesOnWiFiOnly(bool val) { | ||||
|     prefs?.setBool('bgUpdatesOnWiFiOnly', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   DateTime get lastBGCheckTime { | ||||
|     int? temp = prefs?.getInt('lastBGCheckTime'); | ||||
|     return temp != null | ||||
|         ? DateTime.fromMillisecondsSinceEpoch(temp) | ||||
|         : DateTime.fromMillisecondsSinceEpoch(0); | ||||
|   } | ||||
|  | ||||
|   set lastBGCheckTime(DateTime val) { | ||||
|     prefs?.setInt('lastBGCheckTime', val.millisecondsSinceEpoch); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get showDebugOpts { | ||||
|     return prefs?.getBool('showDebugOpts') ?? false; | ||||
|   } | ||||
|  | ||||
|   set showDebugOpts(bool val) { | ||||
|     prefs?.setBool('showDebugOpts', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   bool get highlightTouchTargets { | ||||
|     return prefs?.getBool('highlightTouchTargets') ?? false; | ||||
|   } | ||||
|  | ||||
|   set highlightTouchTargets(bool val) { | ||||
|     prefs?.setBool('highlightTouchTargets', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   Future<Uri?> getExportDir() async { | ||||
|     var uriString = prefs?.getString('exportDir'); | ||||
|     if (uriString != null) { | ||||
|       Uri? uri = Uri.parse(uriString); | ||||
|       if (!(await saf.canRead(uri) ?? false) || | ||||
|           !(await saf.canWrite(uri) ?? false)) { | ||||
|         uri = null; | ||||
|         prefs?.remove('exportDir'); | ||||
|         notifyListeners(); | ||||
|       } | ||||
|       return uri; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> pickExportDir({bool remove = false}) async { | ||||
|     var existingSAFPerms = (await saf.persistedUriPermissions()) ?? []; | ||||
|     var currentOneWayDataSyncDir = await getExportDir(); | ||||
|     Uri? newOneWayDataSyncDir; | ||||
|     if (!remove) { | ||||
|       newOneWayDataSyncDir = (await saf.openDocumentTree()); | ||||
|     } | ||||
|     if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) { | ||||
|       if (newOneWayDataSyncDir == null) { | ||||
|         prefs?.remove('exportDir'); | ||||
|       } else { | ||||
|         prefs?.setString('exportDir', newOneWayDataSyncDir.toString()); | ||||
|       } | ||||
|       notifyListeners(); | ||||
|     } | ||||
|     for (var e in existingSAFPerms) { | ||||
|       await saf.releasePersistableUriPermission(e.uri); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool get autoExportOnChanges { | ||||
|     return prefs?.getBool('autoExportOnChanges') ?? false; | ||||
|   } | ||||
|  | ||||
|   set autoExportOnChanges(bool val) { | ||||
|     prefs?.setBool('autoExportOnChanges', val); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,11 +9,13 @@ import 'package:html/dom.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/apkmirror.dart'; | ||||
| import 'package:obtainium/app_sources/apkpure.dart'; | ||||
| import 'package:obtainium/app_sources/aptoide.dart'; | ||||
| import 'package:obtainium/app_sources/codeberg.dart'; | ||||
| import 'package:obtainium/app_sources/fdroid.dart'; | ||||
| import 'package:obtainium/app_sources/fdroidrepo.dart'; | ||||
| import 'package:obtainium/app_sources/github.dart'; | ||||
| import 'package:obtainium/app_sources/gitlab.dart'; | ||||
| import 'package:obtainium/app_sources/huaweiappgallery.dart'; | ||||
| import 'package:obtainium/app_sources/izzyondroid.dart'; | ||||
| import 'package:obtainium/app_sources/html.dart'; | ||||
| import 'package:obtainium/app_sources/jenkins.dart'; | ||||
| @@ -24,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart'; | ||||
| import 'package:obtainium/app_sources/sourcehut.dart'; | ||||
| import 'package:obtainium/app_sources/steammobile.dart'; | ||||
| import 'package:obtainium/app_sources/telegramapp.dart'; | ||||
| import 'package:obtainium/app_sources/uptodown.dart'; | ||||
| import 'package:obtainium/app_sources/vlc.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| @@ -324,21 +327,29 @@ abstract class AppSource { | ||||
|   bool enforceTrackOnly = false; | ||||
|   bool changeLogIfAnyIsMarkDown = true; | ||||
|   bool appIdInferIsOptional = false; | ||||
|   bool allowSubDomains = false; | ||||
|  | ||||
|   AppSource() { | ||||
|     name = runtimeType.toString(); | ||||
|   } | ||||
|  | ||||
|   overrideVersionDetectionFormDefault(String vd, bool disableStandard) { | ||||
|   overrideVersionDetectionFormDefault(String vd, | ||||
|       {bool disableStandard = false, bool disableRelDate = false}) { | ||||
|     additionalAppSpecificSourceAgnosticSettingFormItems = | ||||
|         additionalAppSpecificSourceAgnosticSettingFormItems.map((e) { | ||||
|       return e.map((e2) { | ||||
|         if (e2.key == 'versionDetection') { | ||||
|           var item = e2 as GeneratedFormDropdown; | ||||
|           item.defaultValue = vd; | ||||
|           item.disabledOptKeys = []; | ||||
|           if (disableStandard) { | ||||
|             item.disabledOptKeys = ['standardVersionDetection']; | ||||
|             item.disabledOptKeys?.add('standardVersionDetection'); | ||||
|           } | ||||
|           if (disableRelDate) { | ||||
|             item.disabledOptKeys?.add('releaseDateAsVersion'); | ||||
|           } | ||||
|           item.disabledOptKeys = | ||||
|               item.disabledOptKeys?.where((element) => element != vd).toList(); | ||||
|         } | ||||
|         return e2; | ||||
|       }).toList(); | ||||
| @@ -353,12 +364,24 @@ abstract class AppSource { | ||||
|     return url; | ||||
|   } | ||||
|  | ||||
|   Map<String, String>? get requestHeaders => null; | ||||
|   Future<Map<String, String>?> getRequestHeaders( | ||||
|       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||
|       bool forAPKDownload = false}) async { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<Response> sourceRequest(String url) async { | ||||
|     if (requestHeaders != null) { | ||||
|   Future<Response> sourceRequest(String url, | ||||
|       {bool followRedirects = true, | ||||
|       Map<String, dynamic> additionalSettings = | ||||
|           const <String, dynamic>{}}) async { | ||||
|     var requestHeaders = | ||||
|         await getRequestHeaders(additionalSettings: additionalSettings); | ||||
|     if (requestHeaders != null || followRedirects == false) { | ||||
|       var req = Request('GET', Uri.parse(url)); | ||||
|       req.headers.addAll(requestHeaders!); | ||||
|       req.followRedirects = followRedirects; | ||||
|       if (requestHeaders != null) { | ||||
|         req.headers.addAll(requestHeaders); | ||||
|       } | ||||
|       return Response.fromStream(await Client().send(req)); | ||||
|     } else { | ||||
|       return get(Uri.parse(url)); | ||||
| @@ -413,7 +436,11 @@ abstract class AppSource { | ||||
|       GeneratedFormSwitch('autoApkFilterByArch', | ||||
|           label: tr('autoApkFilterByArch'), defaultValue: true) | ||||
|     ], | ||||
|     [GeneratedFormTextField('appName', label: tr('appName'), required: false)] | ||||
|     [GeneratedFormTextField('appName', label: tr('appName'), required: false)], | ||||
|     [ | ||||
|       GeneratedFormSwitch('exemptFromBackgroundUpdates', | ||||
|           label: tr('exemptFromBackgroundUpdates')) | ||||
|     ] | ||||
|   ]; | ||||
|  | ||||
|   // Previous 2 variables combined into one at runtime for convenient usage | ||||
| @@ -494,6 +521,20 @@ regExValidator(String? value) { | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| intValidator(String? value, {bool positive = false}) { | ||||
|   if (value == null) { | ||||
|     return tr('invalidInput'); | ||||
|   } | ||||
|   var num = int.tryParse(value); | ||||
|   if (num == null) { | ||||
|     return tr('invalidInput'); | ||||
|   } | ||||
|   if (positive && num <= 0) { | ||||
|     return tr('invalidInput'); | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| class SourceProvider { | ||||
|   // Add more source classes here so they are available via the service | ||||
|   List<AppSource> get sources => [ | ||||
| @@ -501,13 +542,16 @@ class SourceProvider { | ||||
|         GitLab(), | ||||
|         Codeberg(), | ||||
|         FDroid(), | ||||
|         IzzyOnDroid(), | ||||
|         FDroidRepo(), | ||||
|         Jenkins(), | ||||
|         IzzyOnDroid(), | ||||
|         SourceForge(), | ||||
|         SourceHut(), | ||||
|         APKMirror(), | ||||
|         APKPure(), | ||||
|         Aptoide(), | ||||
|         Uptodown(), | ||||
|         APKMirror(), | ||||
|         HuaweiAppGallery(), | ||||
|         Jenkins(), | ||||
|         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) | ||||
|         Mullvad(), | ||||
|         Signal(), | ||||
| @@ -537,7 +581,9 @@ class SourceProvider { | ||||
|     } | ||||
|     AppSource? source; | ||||
|     for (var s in sources.where((element) => element.host != null)) { | ||||
|       if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) { | ||||
|       if (RegExp( | ||||
|               '://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?') | ||||
|           .hasMatch(url)) { | ||||
|         source = s; | ||||
|         break; | ||||
|       } | ||||
|   | ||||
							
								
								
									
										270
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										270
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -22,26 +22,34 @@ packages: | ||||
|     description: | ||||
|       path: "." | ||||
|       ref: main | ||||
|       resolved-ref: f09c79eee5be3c60b04760143eb954a13fdd07f1 | ||||
|       resolved-ref: ba2aa7a11edc2649d1d80c25ed9291521262f714 | ||||
|       url: "https://github.com/ImranR98/android_package_installer" | ||||
|     source: git | ||||
|     version: "0.0.1" | ||||
|   android_package_manager: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: android_package_manager | ||||
|       sha256: b873fe5856f7c442aca9751dac05d117285be9e4de08eb15d1ffb811fd1b688d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.0" | ||||
|   animations: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: animations | ||||
|       sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 | ||||
|       sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.7" | ||||
|     version: "2.0.8" | ||||
|   archive: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: archive | ||||
|       sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" | ||||
|       sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.3.7" | ||||
|     version: "3.3.9" | ||||
|   args: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -102,10 +110,26 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: collection | ||||
|       sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" | ||||
|       sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.17.1" | ||||
|     version: "1.17.2" | ||||
|   connectivity_plus: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: connectivity_plus | ||||
|       sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.0.2" | ||||
|   connectivity_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: connectivity_plus_platform_interface | ||||
|       sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.4" | ||||
|   convert: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -118,10 +142,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cross_file | ||||
|       sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" | ||||
|       sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.3.3+4" | ||||
|     version: "0.3.3+5" | ||||
|   crypto: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -142,10 +166,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: cupertino_icons | ||||
|       sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be | ||||
|       sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.5" | ||||
|     version: "1.0.6" | ||||
|   dbus: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -174,18 +198,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dynamic_color | ||||
|       sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d | ||||
|       sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.6.6" | ||||
|     version: "1.6.7" | ||||
|   easy_localization: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: easy_localization | ||||
|       sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645" | ||||
|       sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|     version: "3.0.3" | ||||
|   easy_logger: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -206,10 +230,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: ffi | ||||
|       sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 | ||||
|       sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|     version: "2.1.0" | ||||
|   file: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -222,10 +246,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" | ||||
|       sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.3.3" | ||||
|     version: "5.5.0" | ||||
|   flutter: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -259,18 +283,18 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_lints | ||||
|       sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" | ||||
|       sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|     version: "2.0.3" | ||||
|   flutter_local_notifications: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_local_notifications | ||||
|       sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975" | ||||
|       sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.1.0+1" | ||||
|     version: "15.1.1" | ||||
|   flutter_local_notifications_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -296,18 +320,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       sha256: "4b1bfbb802d76320a1a46d9ce984106135093efd9d969765d07c2125af107bdf" | ||||
|       sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.17" | ||||
|     version: "0.6.17+3" | ||||
|   flutter_plugin_android_lifecycle: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" | ||||
|       sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.15" | ||||
|     version: "2.0.16" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
| @@ -366,22 +390,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.0.17" | ||||
|   installed_apps: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: installed_apps | ||||
|       sha256: "145af8eb6e4e7c830e9888d6de0573ae5c139e8e0742a3e67316e1db21ab6fe0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.1" | ||||
|   intl: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: intl | ||||
|       sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 | ||||
|       sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.18.0" | ||||
|     version: "0.18.1" | ||||
|   js: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -418,18 +434,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: matcher | ||||
|       sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" | ||||
|       sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.15" | ||||
|     version: "0.12.16" | ||||
|   material_color_utilities: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: material_color_utilities | ||||
|       sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 | ||||
|       sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|     version: "0.5.0" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -454,22 +470,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0" | ||||
|   package_archive_info: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: package_archive_info | ||||
|       sha256: "8f671a29b79d15f192e5e2b0dab9d0bad66b9ee93fb58d4e0afdb62f91a259be" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.0" | ||||
|   package_info: | ||||
|   nm: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: package_info | ||||
|       sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" | ||||
|       name: nm | ||||
|       sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.2" | ||||
|     version: "0.5.0" | ||||
|   path: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -482,66 +490,66 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: path_provider | ||||
|       sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" | ||||
|       sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.15" | ||||
|     version: "2.1.1" | ||||
|   path_provider_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_android | ||||
|       sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" | ||||
|       sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|     version: "2.2.0" | ||||
|   path_provider_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_foundation | ||||
|       sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" | ||||
|       sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|     version: "2.3.1" | ||||
|   path_provider_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_linux | ||||
|       sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 | ||||
|       sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|     version: "2.2.1" | ||||
|   path_provider_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_platform_interface | ||||
|       sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 | ||||
|       sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|     version: "2.1.1" | ||||
|   path_provider_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: path_provider_windows | ||||
|       sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da | ||||
|       sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|     version: "2.2.1" | ||||
|   permission_handler: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: permission_handler | ||||
|       sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" | ||||
|       sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.4.3" | ||||
|     version: "11.0.0" | ||||
|   permission_handler_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_android | ||||
|       sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" | ||||
|       sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.3.3" | ||||
|     version: "11.0.1" | ||||
|   permission_handler_apple: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -554,10 +562,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: permission_handler_platform_interface | ||||
|       sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" | ||||
|       sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.11.3" | ||||
|     version: "3.11.5" | ||||
|   permission_handler_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -578,18 +586,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: platform | ||||
|       sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" | ||||
|       sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.0" | ||||
|     version: "3.1.2" | ||||
|   plugin_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: plugin_platform_interface | ||||
|       sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" | ||||
|       sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.5" | ||||
|     version: "2.1.6" | ||||
|   pointycastle: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -626,58 +634,66 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" | ||||
|       sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|     version: "2.2.1" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 | ||||
|       sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|     version: "2.2.1" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_foundation | ||||
|       sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 | ||||
|       sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|     version: "2.3.4" | ||||
|   shared_preferences_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_linux | ||||
|       sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" | ||||
|       sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|     version: "2.3.1" | ||||
|   shared_preferences_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_platform_interface | ||||
|       sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" | ||||
|       sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|     version: "2.3.1" | ||||
|   shared_preferences_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_web | ||||
|       sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" | ||||
|       sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|     version: "2.2.1" | ||||
|   shared_preferences_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_windows | ||||
|       sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d | ||||
|       sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|     version: "2.3.1" | ||||
|   shared_storage: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_storage | ||||
|       sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.0" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -687,10 +703,10 @@ 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: | ||||
| @@ -751,10 +767,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: test_api | ||||
|       sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb | ||||
|       sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.1" | ||||
|     version: "0.6.0" | ||||
|   timezone: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -775,66 +791,66 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: url_launcher | ||||
|       sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" | ||||
|       sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.1.12" | ||||
|     version: "6.1.14" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" | ||||
|       sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.37" | ||||
|     version: "6.1.0" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_ios | ||||
|       sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" | ||||
|       sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.1.4" | ||||
|     version: "6.1.5" | ||||
|   url_launcher_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_linux | ||||
|       sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" | ||||
|       sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.5" | ||||
|     version: "3.0.6" | ||||
|   url_launcher_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_macos | ||||
|       sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" | ||||
|       sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.6" | ||||
|     version: "3.0.7" | ||||
|   url_launcher_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_platform_interface | ||||
|       sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea | ||||
|       sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.3" | ||||
|     version: "2.1.5" | ||||
|   url_launcher_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_web | ||||
|       sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 | ||||
|       sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.18" | ||||
|     version: "2.0.20" | ||||
|   url_launcher_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_windows | ||||
|       sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" | ||||
|       sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.7" | ||||
|     version: "3.0.8" | ||||
|   uuid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -851,46 +867,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.4" | ||||
|   web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: web | ||||
|       sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.4-beta" | ||||
|   webview_flutter: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: webview_flutter | ||||
|       sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00" | ||||
|       sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.2" | ||||
|     version: "4.2.4" | ||||
|   webview_flutter_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       sha256: d936a09fbfd08cb78f7329e0bbacf6158fbdfe24ffc908b22444c07d295eb193 | ||||
|       sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.9.2" | ||||
|     version: "3.10.1" | ||||
|   webview_flutter_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_platform_interface | ||||
|       sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30" | ||||
|       sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|     version: "2.6.0" | ||||
|   webview_flutter_wkwebview: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_wkwebview | ||||
|       sha256: "5fa098f28b606f699e8ca52d9e4e11edbbfef65189f5f77ae92703ba5408fd25" | ||||
|       sha256: d2f7241849582da80b79acb03bb936422412ce5c0c79fb5f6a1de5421a5aecc4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.7.2" | ||||
|     version: "3.7.4" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 | ||||
|       sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.0.6" | ||||
|     version: "5.0.7" | ||||
|   win32_registry: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -903,10 +927,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: xdg_directories | ||||
|       sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff | ||||
|       sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.1" | ||||
|     version: "1.0.3" | ||||
|   xml: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -924,5 +948,5 @@ packages: | ||||
|     source: hosted | ||||
|     version: "3.1.2" | ||||
| sdks: | ||||
|   dart: ">=3.0.0 <4.0.0" | ||||
|   flutter: ">=3.10.0" | ||||
|   dart: ">=3.1.0 <4.0.0" | ||||
|   flutter: ">=3.13.0" | ||||
|   | ||||
							
								
								
									
										13
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| name: obtainium | ||||
| description: A new Flutter project. | ||||
| description: Get Android App Updates Directly From the Source. | ||||
|  | ||||
| # The following line prevents the package from being accidentally published to | ||||
| # pub.dev using `flutter pub publish`. This is preferred for private packages. | ||||
| @@ -17,10 +17,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 0.13.23+187 # When changing this, update the tag in main() accordingly | ||||
| version: 0.14.15+207 # When changing this, update the tag in main() accordingly | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=2.18.2 <3.0.0' | ||||
|   sdk: '>=3.0.0 <4.0.0' | ||||
|  | ||||
| # Dependencies specify other packages that your package needs in order to work. | ||||
| # To automatically upgrade your package dependencies to the latest versions | ||||
| @@ -46,7 +46,7 @@ dependencies: | ||||
|   html: ^0.15.0 | ||||
|   shared_preferences: ^2.0.15 | ||||
|   url_launcher: ^6.1.5 | ||||
|   permission_handler: ^10.0.0 | ||||
|   permission_handler: ^11.0.0 | ||||
|   fluttertoast: ^8.0.9 | ||||
|   device_info_plus: ^9.0.0 | ||||
|   file_picker: ^5.2.10 | ||||
| @@ -55,9 +55,8 @@ dependencies: | ||||
|     git: | ||||
|       url: https://github.com/ImranR98/android_package_installer | ||||
|       ref: main | ||||
|   android_package_manager: ^0.6.0 | ||||
|   share_plus: ^7.0.0 | ||||
|   installed_apps: ^1.3.1 | ||||
|   package_archive_info: ^0.1.0 | ||||
|   android_alarm_manager_plus: ^3.0.0 | ||||
|   sqflite: ^2.2.0+3 | ||||
|   easy_localization: ^3.0.1 | ||||
| @@ -65,6 +64,8 @@ dependencies: | ||||
|   flutter_markdown: ^0.6.14 | ||||
|   flutter_archive: ^5.0.0 | ||||
|   hsluv: ^1.1.3 | ||||
|   connectivity_plus: ^4.0.2 | ||||
|   shared_storage: ^0.8.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user