mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Compare commits
	
		
			116 Commits
		
	
	
		
			v0.13.24-b
			...
			v0.14.10-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | 
							
								
								
									
										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. | ||||||
| @@ -16,13 +16,14 @@ Currently supported App sources: | |||||||
| - [Signal](https://signal.org/) | - [Signal](https://signal.org/) | ||||||
| - [SourceForge](https://sourceforge.net/) | - [SourceForge](https://sourceforge.net/) | ||||||
| - [SourceHut](https://git.sr.ht/) | - [SourceHut](https://git.sr.ht/) | ||||||
|  | - [Aptoide](https://aptoide.com/) | ||||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||||
| - [APKPure](https://apkpure.com/) | - [APKPure](https://apkpure.com/) | ||||||
|  | - [Huawei AppGallery](https://appgallery.huawei.com/) | ||||||
| - Third Party F-Droid Repos | - Third Party F-Droid Repos | ||||||
| - Jenkins Jobs | - Jenkins Jobs | ||||||
| - [Steam](https://store.steampowered.com/mobile) | - [Steam](https://store.steampowered.com/mobile) | ||||||
| - [Telegram App](https://telegram.org) | - [Telegram App](https://telegram.org) | ||||||
| - [VLC](https://www.videolan.org/vlc/download-android.html) |  | ||||||
| - [Neutron Code](https://neutroncode.com) | - [Neutron Code](https://neutroncode.com) | ||||||
| - "HTML" (Fallback) | - "HTML" (Fallback) | ||||||
|   - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) |   - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) | ||||||
| @@ -33,8 +34,9 @@ Currently supported App sources: | |||||||
|     alt="Get it on GitHub" |     alt="Get it on GitHub" | ||||||
|     height="80">](https://github.com/ImranR98/Obtainium/releases) |     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 | ## 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. | - 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 | ## Screenshots | ||||||
|   | |||||||
| @@ -49,7 +49,6 @@ android { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |  | ||||||
|         applicationId "dev.imranr.obtainium" |         applicationId "dev.imranr.obtainium" | ||||||
|         // You can update the following values to match your application needs. |         // 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. |         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. | ||||||
|   | |||||||
| @@ -71,4 +71,5 @@ | |||||||
|     <uses-permission |     <uses-permission | ||||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" |         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||||
|         android:maxSdkVersion="29"/> |         android:maxSdkVersion="29"/> | ||||||
|  |     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> | ||||||
| </manifest> | </manifest> | ||||||
							
								
								
									
										308
									
								
								assets/translations/br.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								assets/translations/br.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | |||||||
|  | { | ||||||
|  |     "invalidURLForSource": "URL {} inválida", | ||||||
|  |     "noReleaseFound": "Não foi possivel encontrar uma versão adequada", | ||||||
|  |     "noVersionFound": "Não foi possivel encontrar uma versão lançada", | ||||||
|  |     "urlMatchesNoSource": "URL não corresponde a uma fonte conhecida", | ||||||
|  |     "cantInstallOlderVersion": "Não pode instalar uma versão anterior de um App", | ||||||
|  |     "appIdMismatch": "ID do pacote baixado não é igual ao ID do App instalado", | ||||||
|  |     "functionNotImplemented": "Esta classe não implementou essa função", | ||||||
|  |     "placeholder": "Espaço Reservado", | ||||||
|  |     "someErrors": "Alguns Erros Ocorreram", | ||||||
|  |     "unexpectedError": "Erro Inesperado", | ||||||
|  |     "ok": "Ok", | ||||||
|  |     "and": "e", | ||||||
|  |     "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", | ||||||
|  |     "githubPATHint": "O TAP deve estar nesse formato: usuario:token", | ||||||
|  |     "githubPATFormat": "usuario:token", | ||||||
|  |     "includePrereleases": "Incluir pré-lançamentos", | ||||||
|  |     "fallbackToOlderReleases": "Retornar para versões anteriores", | ||||||
|  |     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", | ||||||
|  |     "invalidRegEx": "Expressão Regular Inválida", | ||||||
|  |     "noDescription": "Sem descrição", | ||||||
|  |     "cancel": "Cancelar", | ||||||
|  |     "continue": "Continuar", | ||||||
|  |     "requiredInBrackets": "(Necessário)", | ||||||
|  |     "dropdownNoOptsError": "ERRO: O DROPDOWN DEVE TER PELO MENOS UMA OPÇÃO", | ||||||
|  |     "colour": "Cor", | ||||||
|  |     "githubStarredRepos": "Favoritados no GitHub", | ||||||
|  |     "uname": "Nome de usuário", | ||||||
|  |     "wrongArgNum": "Número de argumentos errado", | ||||||
|  |     "xIsTrackOnly": "{} é 'Apenas Seguir'", | ||||||
|  |     "source": "Fonte", | ||||||
|  |     "app": "App", | ||||||
|  |     "appsFromSourceAreTrackOnly": "Os apps desta fonte são 'Apenas Seguir'.", | ||||||
|  |     "youPickedTrackOnly": "Você selecionou a opção 'Apenas Seguir'.", | ||||||
|  |     "trackOnlyAppDescription": "Esse App vai ser seguido por atualizações, mais o Obtainium não poderá baixa-lo ou instala-lo.", | ||||||
|  |     "cancelled": "Cancelado", | ||||||
|  |     "appAlreadyAdded": "App já adicionado", | ||||||
|  |     "alreadyUpToDateQuestion": "App já atualizado?", | ||||||
|  |     "addApp": "Adicionar App", | ||||||
|  |     "appSourceURL": "URL de origem do App", | ||||||
|  |     "error": "Erro", | ||||||
|  |     "add": "Adicionar", | ||||||
|  |     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", | ||||||
|  |     "search": "Procurar", | ||||||
|  |     "additionalOptsFor": "Opções Adicionais para {}", | ||||||
|  |     "supportedSourcesBelow": "Fontes Compatíveis:", | ||||||
|  |     "trackOnlyInBrackets": "(Apenas Seguir)", | ||||||
|  |     "searchableInBrackets": "(Pesquisável)", | ||||||
|  |     "appsString": "Apps", | ||||||
|  |     "noApps": "Sem Apps", | ||||||
|  |     "noAppsForFilter": "Sem Apps para Filtrar", | ||||||
|  |     "byX": "Por {}", | ||||||
|  |     "percentProgress": "Progresso: {}%", | ||||||
|  |     "pleaseWait": "Por Favor Espere", | ||||||
|  |     "updateAvailable": "Atualização Disponível", | ||||||
|  |     "estimateInBracketsShort": "(Aprox.)", | ||||||
|  |     "notInstalled": "Não Instalado", | ||||||
|  |     "estimateInBrackets": "(Aproximado)", | ||||||
|  |     "selectAll": "Selecionar All", | ||||||
|  |     "deselectN": "Deselecionar {}", | ||||||
|  |     "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.", | ||||||
|  |     "removeSelectedAppsQuestion": "Remover Apps Selecionados?", | ||||||
|  |     "removeSelectedApps": "Remover Apps Selecionados", | ||||||
|  |     "updateX": "Atualizar {}", | ||||||
|  |     "installX": "Instalar {}", | ||||||
|  |     "markXTrackOnlyAsUpdated": "Marcar {}\n(Apenas Seguir)\ncomo Atualizado", | ||||||
|  |     "changeX": "Mudar {}", | ||||||
|  |     "installUpdateApps": "Instalar/Atualizar Apps", | ||||||
|  |     "installUpdateSelectedApps": "Instalar/Atualizar Apps Selecionados", | ||||||
|  |     "markXSelectedAppsAsUpdated": "Marcar {} Apps Delecionados como Atualizados?", | ||||||
|  |     "no": "Não", | ||||||
|  |     "yes": "Sim", | ||||||
|  |     "markSelectedAppsUpdated": "Marcar Apps Selecionados como Atualizados", | ||||||
|  |     "pinToTop": "Fixar no topo", | ||||||
|  |     "unpinFromTop": "Desafixar do topo", | ||||||
|  |     "resetInstallStatusForSelectedAppsQuestion": "Reiniciar Status de Instalação para Apps Seleciondos?", | ||||||
|  |     "installStatusOfXWillBeResetExplanation": "O status de instalação de qualquer app selecionado sera reiniciado.\n\nIsso pode ajudar quando uma versão de um App mostrada no Obtainium é incorreta devido a falhas ao atualizar ou outros problemas.", | ||||||
|  |     "shareSelectedAppURLs": "Compartilhar URLs de Apps Selecionados", | ||||||
|  |     "resetInstallStatus": "Reiniciar Status de Instalação", | ||||||
|  |     "more": "Mais", | ||||||
|  |     "removeOutdatedFilter": "Remover Filtro de Apps Desatualizados", | ||||||
|  |     "showOutdatedOnly": "Mostrar Apenas Apps Desatualizados", | ||||||
|  |     "filter": "Filtro", | ||||||
|  |     "filterActive": "Filtro *", | ||||||
|  |     "filterApps": "Filtrar Apps", | ||||||
|  |     "appName": "Nome do App", | ||||||
|  |     "author": "Autor", | ||||||
|  |     "upToDateApps": "Apps Atualizados", | ||||||
|  |     "nonInstalledApps": "Apps Não Instalados", | ||||||
|  |     "importExport": "Importar/Exportar", | ||||||
|  |     "settings": "Configurações", | ||||||
|  |     "exportedTo": "Exportado para {}", | ||||||
|  |     "obtainiumExport": "Exportar Obtainium", | ||||||
|  |     "invalidInput": "Input Inválido", | ||||||
|  |     "importedX": "Importado {}", | ||||||
|  |     "obtainiumImport": "Importar Obtainium", | ||||||
|  |     "importFromURLList": "Importar de Lista de URLs", | ||||||
|  |     "searchQuery": "Pesquisa", | ||||||
|  |     "appURLList": "Lista de URLs de Apps", | ||||||
|  |     "line": "Linha", | ||||||
|  |     "searchX": "Pesquisa {}", | ||||||
|  |     "noResults": "Nenhum resultado encontrado", | ||||||
|  |     "importX": "Importar {}", | ||||||
|  |     "importedAppsIdDisclaimer": "Apps Importados podem ser mostrados incorretamente como \"Não Instalado\".\nPara consertar, reinstale-os usando o Obtainium.\nIsso não deve afetar dados do App.\n\nAfeta apenas métodos de importação de URL e de terceiros.", | ||||||
|  |     "importErrors": "Erros de Importação", | ||||||
|  |     "importedXOfYApps": "{} de {} Apps importados.", | ||||||
|  |     "followingURLsHadErrors": "As seguintes URLs apresentaram erros:", | ||||||
|  |     "okay": "Ok", | ||||||
|  |     "selectURL": "Selecionar URL", | ||||||
|  |     "selectURLs": "Selecionar URLs", | ||||||
|  |     "pick": "Escolher", | ||||||
|  |     "theme": "Tema", | ||||||
|  |     "dark": "Escuro", | ||||||
|  |     "light": "Claro", | ||||||
|  |     "followSystem": "Seguir o Sistema", | ||||||
|  |     "obtainium": "Obtainium", | ||||||
|  |     "materialYou": "Material You", | ||||||
|  |     "useBlackTheme": "Usar tema preto completamente escuro", | ||||||
|  |     "appSortBy": "Classificar App por", | ||||||
|  |     "authorName": "Autor/Nome", | ||||||
|  |     "nameAuthor": "Nome/Autor", | ||||||
|  |     "asAdded": "Como Adicionado", | ||||||
|  |     "appSortOrder": "Ordem de classificação de Apps", | ||||||
|  |     "ascending": "Ascendente", | ||||||
|  |     "descending": "Descendente", | ||||||
|  |     "bgUpdateCheckInterval": "Intervalo de verificação de atualizações em segundo plano", | ||||||
|  |     "neverManualOnly": "Nunca - Apenas Manual", | ||||||
|  |     "appearance": "Aparência", | ||||||
|  |     "showWebInAppView": "Mostrar páginas da internet em App view", | ||||||
|  |     "pinUpdates": "Fixar atualizações no topo da visão de Apps", | ||||||
|  |     "updates": "Atualizações", | ||||||
|  |     "sourceSpecific": "Específico a fonte", | ||||||
|  |     "appSource": "Fonte de Apps", | ||||||
|  |     "noLogs": "Sem Logs", | ||||||
|  |     "appLogs": "Logs do App", | ||||||
|  |     "close": "Fechar", | ||||||
|  |     "share": "Compartilhar", | ||||||
|  |     "appNotFound": "App não encontrado", | ||||||
|  |     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||||
|  |     "pickAnAPK": "Selecionar um APK", | ||||||
|  |     "appHasMoreThanOnePackage": "{} tem mais de um pacote:", | ||||||
|  |     "deviceSupportsXArch": "Seu dispositivo suporta a arquitetura de CPU {}.", | ||||||
|  |     "deviceSupportsFollowingArchs": "Seu dispositivo suporta as seguintes arquiteturas de CPU:", | ||||||
|  |     "warning": "Aviso", | ||||||
|  |     "sourceIsXButPackageFromYPrompt": "A Fonte do App é '{}' mais o pacote lançado vem de '{}'. Continuar?", | ||||||
|  |     "updatesAvailable": "Atualizações Disponíveis", | ||||||
|  |     "updatesAvailableNotifDescription": "Notifica o usuário quando atualizações estão disponíveis um ou mais Apps seguidos pelo Obtainium", | ||||||
|  |     "noNewUpdates": "Sem novas atualizações.", | ||||||
|  |     "xHasAnUpdate": "{} tem uma atualização.", | ||||||
|  |     "appsUpdated": "Apps Atualizados", | ||||||
|  |     "appsUpdatedNotifDescription": "Notifica o usuário quando atualizações para um ou mais Apps foram aplicadas em segundo plano", | ||||||
|  |     "xWasUpdatedToY": "{} foi atualizado para {}.", | ||||||
|  |     "errorCheckingUpdates": "Erro ao Procurar por Atualizações", | ||||||
|  |     "errorCheckingUpdatesNotifDescription": "Uma notificação que mostra quando a checagem por atualizações em segundo plano falha", | ||||||
|  |     "appsRemoved": "Apps Removidos", | ||||||
|  |     "appsRemovedNotifDescription": "Notifica o usuário quando um ou mais Apps foram removidos devido a erros ao carregá-los", | ||||||
|  |     "xWasRemovedDueToErrorY": "{} foi removido devido a este erro: {}", | ||||||
|  |     "completeAppInstallation": "Instalação completa do App", | ||||||
|  |     "obtainiumMustBeOpenToInstallApps": "Obtainium deve estar aberto para instalar Apps", | ||||||
|  |     "completeAppInstallationNotifDescription": "Pede ao usuário que retorne ao Obtainium para finalizar a instalação de um App", | ||||||
|  |     "checkingForUpdates": "Checando por Atualizações", | ||||||
|  |     "checkingForUpdatesNotifDescription": "Notificação transiente que aparece quando checando por atualizações", | ||||||
|  |     "pleaseAllowInstallPerm": "Por favor, permita o Obtainium instalar Apps", | ||||||
|  |     "trackOnly": "Apenas Seguir", | ||||||
|  |     "errorWithHttpStatusCode": "Erro {}", | ||||||
|  |     "versionCorrectionDisabled": "Correção de versão desativada (plugin parece não funcionar)", | ||||||
|  |     "unknown": "Desconhecido", | ||||||
|  |     "none": "Nenhum", | ||||||
|  |     "never": "Nunca", | ||||||
|  |     "latestVersionX": "Última versão: {}", | ||||||
|  |     "installedVersionX": "Versão Instalada: {}", | ||||||
|  |     "lastUpdateCheckX": "Última Checagem por Atualização: {}", | ||||||
|  |     "remove": "Remover", | ||||||
|  |     "yesMarkUpdated": "Sim, Marcar como Atualizado", | ||||||
|  |     "fdroid": "F-Droid Official", | ||||||
|  |     "appIdOrName": "ID do App ou Nome", | ||||||
|  |     "appId": "ID do App", | ||||||
|  |     "appWithIdOrNameNotFound": "Nenhum App foi encontrado com esse ID ou nome", | ||||||
|  |     "reposHaveMultipleApps": "Repositórios podem conter multiplos Apps", | ||||||
|  |     "fdroidThirdPartyRepo": "Repositórios de terceiros F-Droid", | ||||||
|  |     "steam": "Steam", | ||||||
|  |     "steamMobile": "Steam Mobile", | ||||||
|  |     "steamChat": "Steam Chat", | ||||||
|  |     "install": "Instalar", | ||||||
|  |     "markInstalled": "Marcar Instalado", | ||||||
|  |     "update": "Atualizar", | ||||||
|  |     "markUpdated": "Marcar Atualizado", | ||||||
|  |     "additionalOptions": "Opções Adicionais", | ||||||
|  |     "disableVersionDetection": "Desativar Detecção de Versão", | ||||||
|  |     "noVersionDetectionExplanation": "Essa opção deve apenas ser usada por Apps onde detecção de versão não funciona corretamente.", | ||||||
|  |     "downloadingX": "Baixando {}", | ||||||
|  |     "downloadNotifDescription": "Notifica o usuário do progresso ao baixar um App", | ||||||
|  |     "noAPKFound": "APK não encontrado", | ||||||
|  |     "noVersionDetection": "Sem Detecção de versão", | ||||||
|  |     "categorize": "Categorizar", | ||||||
|  |     "categories": "Categorias", | ||||||
|  |     "category": "Categoria", | ||||||
|  |     "noCategory": "Sem Categoria", | ||||||
|  |     "noCategories": "Sem Categoria", | ||||||
|  |     "deleteCategoriesQuestion": "Deletar Categorias?", | ||||||
|  |     "categoryDeleteWarning": "Todos os Apps em categorias removidas serão descategorizados.", | ||||||
|  |     "addCategory": "Adicionar Categoria", | ||||||
|  |     "label": "Etiqueta", | ||||||
|  |     "language": "Linguagem", | ||||||
|  |     "copiedToClipboard": "Copiado para a área de transferência", | ||||||
|  |     "storagePermissionDenied": "Permição ao armazenamento negada", | ||||||
|  |     "selectedCategorizeWarning": "Isso vai substituir qualquer confirução de categoria para os Apps selecionados.", | ||||||
|  |     "filterAPKsByRegEx": "Filtrar APKs por Expressão Regular", | ||||||
|  |     "removeFromObtainium": "Remover do Obtainium", | ||||||
|  |     "uninstallFromDevice": "Desinstalar do dispositivo", | ||||||
|  |     "onlyWorksWithNonVersionDetectApps": "Apenas funciona para Apps com detecção de versão desativada.", | ||||||
|  |     "releaseDateAsVersion": "Usar Data de Lançamento como Versão", | ||||||
|  |     "releaseDateAsVersionExplanation": "Esta opção só deve ser usada para aplicativos onde a detecção de versão não funciona corretamente, mas há uma data de lançamento disponível.", | ||||||
|  |     "changes": "Mudanças", | ||||||
|  |     "releaseDate": "Data de Lançamento", | ||||||
|  |     "importFromURLsInFile": "Importar de URLs em Arquivo (como OPML)", | ||||||
|  |     "versionDetection": "Detecção de Versão", | ||||||
|  |     "standardVersionDetection": "Detecção de versão padrão", | ||||||
|  |     "groupByCategory": "Agroupar por Categoria", | ||||||
|  |     "autoApkFilterByArch": "Tente filtrar APKs por arquitetura de CPU, se possível", | ||||||
|  |     "overrideSource": "Substituir Fonte", | ||||||
|  |     "dontShowAgain": "Não mostrar isso novamente", | ||||||
|  |     "dontShowTrackOnlyWarnings": "Não mostrar avisos 'Apenas Seguir'", | ||||||
|  |     "dontShowAPKOriginWarnings": "Não mostrar avisos de origem da APK", | ||||||
|  |     "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps", | ||||||
|  |     "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)", | ||||||
|  |     "about": "Sobre", | ||||||
|  |     "requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)", | ||||||
|  |     "checkOnStart": "Checar por atualizações ao iniciar ", | ||||||
|  |     "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte", | ||||||
|  |     "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente", | ||||||
|  |     "pickHighestVersionCode": "Auto-selecionar o maior numero de versão do APK", | ||||||
|  |     "checkUpdateOnDetailPage": "Checar por atualizações ao abrir a pagina de detalhes de um App", | ||||||
|  |     "disablePageTransitions": "Desativar animações de transição de pagina", | ||||||
|  |     "reversePageTransitions": "Reverter animações de transição de pagina", | ||||||
|  |     "minStarCount": "Contagem Minima de Estrelas", | ||||||
|  |     "addInfoBelow": "Adicionar essa informação abaixo.", | ||||||
|  |     "addInfoInSettings": "Adicionar essa informação nas configurações.", | ||||||
|  |     "githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.", | ||||||
|  |     "gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.", | ||||||
|  |     "sortByFileNamesNotLinks": "Classifique por nomes de arquivos em vez de links completos", | ||||||
|  |     "filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular", | ||||||
|  |     "customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')", | ||||||
|  |     "appsPossiblyUpdated": "Tentativas de atualização de Apps", | ||||||
|  |     "appsPossiblyUpdatedNotifDescription": "Notifica o usuário de que atualizações de um ou mais Apps foram potencialmente aplicadas em segundo plano", | ||||||
|  |     "xWasPossiblyUpdatedToY": "{} pode ter sido atualizado para {}.", | ||||||
|  |     "enableBackgroundUpdates": "Ativar atualizações em segundo plano", | ||||||
|  |     "backgroundUpdateReqsExplanation": "Atualizações em segundo plano podem não ser possíveis para todos os Apps.", | ||||||
|  |     "backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.", | ||||||
|  |     "verifyLatestTag": "Verifique a 'ultima' etiqueta", | ||||||
|  |     "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", | ||||||
|  |     "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 {}", |    "invalidURLForSource": "Nije važeći URL aplikacije {}", | ||||||
|    "noReleaseFound": "Nije moguće pronaći odgovarajuće izdanje", |    "noReleaseFound": "Nije moguće pronaći odgovarajuće izdanje", | ||||||
|    "noVersionFound": "Nije moguće odrediti verziju izdanja", |    "noVersionFound": "Nije moguće odrediti verziju izdanja", | ||||||
| @@ -11,12 +11,6 @@ | |||||||
|    "unexpectedError": "Neočekivana greška", |    "unexpectedError": "Neočekivana greška", | ||||||
|    "ok": "Dobro", |    "ok": "Dobro", | ||||||
|    "and": "i", |    "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)", |    "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", |    "githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token", | ||||||
|    "githubPATFormat": "korisničko_ime:token", |    "githubPATFormat": "korisničko_ime:token", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||||
|    "sortByFileNamesNotLinks": "Sort by file names instead of full links", |    "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|    "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |    "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", | ||||||
|    "removeAppQuestion": { |    "removeAppQuestion": { | ||||||
|       "one": "Želite li ukloniti aplikaciju?", |       "one": "Želite li ukloniti aplikaciju?", | ||||||
|       "other": "Želite li ukloniti aplikacije?" |       "other": "Želite li ukloniti aplikacije?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|    "xAndNMoreUpdatesInstalled": { |    "xAndNMoreUpdatesInstalled": { | ||||||
|       "one": "{} i još 1 aplikacija je ažurirana.", |       "one": "{} i još 1 aplikacija je ažurirana.", | ||||||
|       "other": "{} i još {} aplikacija je ažurirano." |       "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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Unerwarteter Fehler", |     "unexpectedError": "Unerwarteter Fehler", | ||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "und", |     "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)", |     "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", | ||||||
|     "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token", |     "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token", | ||||||
|     "githubPATFormat": "Benutzername:Token", |     "githubPATFormat": "Benutzername:Token", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|     "gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel", |     "gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel", | ||||||
|     "sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links", |     "sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links", | ||||||
|     "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern", |     "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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": "Auto-select highest versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "App entfernen?", |         "one": "App entfernen?", | ||||||
|         "other": "Apps entfernen?" |         "other": "Apps entfernen?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} und 1 weitere Anwendung wurden aktualisiert.", |         "one": "{} und 1 weitere Anwendung wurden aktualisiert.", | ||||||
|         "other": "{} und {} weitere Anwendungen 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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Unexpected Error", |     "unexpectedError": "Unexpected Error", | ||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "and", |     "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)", |     "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", | ||||||
|     "githubPATHint": "PAT must be in this format: username:token", |     "githubPATHint": "PAT must be in this format: username:token", | ||||||
|     "githubPATFormat": "username:token", |     "githubPATFormat": "username:token", | ||||||
| @@ -246,7 +240,19 @@ | |||||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remove App?", |         "one": "Remove App?", | ||||||
|         "other": "Remove Apps?" |         "other": "Remove Apps?" | ||||||
| @@ -292,7 +298,11 @@ | |||||||
|         "other": "{} and {} more apps have updates." |         "other": "{} and {} more apps have updates." | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} and 1 more app were updated.", |         "one": "{} and 1 more app was updated.", | ||||||
|         "other": "{} and {} more apps were 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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Error Inesperado", |     "unexpectedError": "Error Inesperado", | ||||||
|     "ok": "Correcto", |     "ok": "Correcto", | ||||||
|     "and": "y", |     "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)", |     "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", | ||||||
|     "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token", |     "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token", | ||||||
|     "githubPATFormat": "nombre_de_usuario:token", |     "githubPATFormat": "nombre_de_usuario:token", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "¿Eliminar Aplicación?", |         "one": "¿Eliminar Aplicación?", | ||||||
|         "other": "¿Eliminar Aplicaciones?" |         "other": "¿Eliminar Aplicaciones?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} y 1 aplicación más han sido actualizadas.", |         "one": "{} y 1 aplicación más han sido actualizadas.", | ||||||
|         "other": "{} y {} aplicaciones 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,12 +11,6 @@ | |||||||
|     "unexpectedError": "خطای غیرمنتظره", |     "unexpectedError": "خطای غیرمنتظره", | ||||||
|     "ok": "باشه", |     "ok": "باشه", | ||||||
|     "and": "و", |     "and": "و", | ||||||
|     "startedBgUpdateTask": "شروع بررسی بروزرسانی BG", |  | ||||||
|     "bgUpdateIgnoreAfterIs": "نادیده گرفتن بروزرسانی BG بعد از {} است", |  | ||||||
|     "startedActualBGUpdateCheck": "بررسی بهروزرسانی واقعی BG آغاز شد", |  | ||||||
|     "bgUpdateTaskFinished": "کار بررسی بهروزرسانی BG تمام شد", |  | ||||||
|     "firstRun": "این اولین اجرای Obtainium است", |  | ||||||
|     "settingUpdateCheckIntervalTo": "تنظیم فاصله بهروزرسانی روی {}", |  | ||||||
|     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", |     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", | ||||||
|     "githubPATHint": "PAT باید در این قالب باشد: username:token", |     "githubPATHint": "PAT باید در این قالب باشد: username:token", | ||||||
|     "githubPATFormat": "username:token", |     "githubPATFormat": "username:token", | ||||||
| @@ -229,24 +223,33 @@ | |||||||
|     "dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید", |     "dontShowTrackOnlyWarnings": "هشدار 'فقط ردیابی' را نشان ندهید", | ||||||
|     "dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید", |     "dontShowAPKOriginWarnings": "هشدارهای منبع APK را نشان ندهید", | ||||||
|     "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید", |     "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید", | ||||||
|     "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو را فعال می کند and Better APK Discovery)", |     "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)", | ||||||
|     "about": "درباره", |     "about": "درباره", | ||||||
|     "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", |     "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", | ||||||
|     "checkOnStart": "بررسی در شروع", |     "checkOnStart": "بررسی در شروع", | ||||||
|     "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید", |     "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید", | ||||||
|     "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", |     "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی", | ||||||
|     "pickHighestVersionCode": "Auto-select highest version code APK", |     "pickHighestVersionCode": "انتخاب خودکار بالاترین کد نسخه APK", | ||||||
|     "checkUpdateOnDetailPage": "Check for updates on opening an App detail page", |     "checkUpdateOnDetailPage": "برای باز کردن صفحه جزئیات برنامه، بهروزرسانیها را بررسی کنید", | ||||||
|     "disablePageTransitions": "Disable page transition animations", |     "disablePageTransitions": "غیرفعال کردن انیمیشن های انتقال صفحه", | ||||||
|     "reversePageTransitions": "Reverse page transition animations", |     "reversePageTransitions": "انیمیشن های انتقال معکوس صفحه", | ||||||
|     "minStarCount": "Minimum Star Count", |     "minStarCount": "حداقل تعداد ستاره", | ||||||
|     "addInfoBelow": "Add this info below.", |     "addInfoBelow": "این اطلاعات را در زیر اضافه کنید", | ||||||
|     "addInfoInSettings": "Add this info in the Settings.", |     "addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.", | ||||||
|     "githubSourceNote": "GitHub rate limiting can be avoided using an API key.", |     "githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.", | ||||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |     "gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "برنامه حذف شود؟", |         "one": "برنامه حذف شود؟", | ||||||
|         "other": "برنامه ها حذف شوند؟" |         "other": "برنامه ها حذف شوند؟" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} و 1 برنامه دیگر به روز شدند.", |         "one": "{} و 1 برنامه دیگر به روز شدند.", | ||||||
|         "other": "{} و {} برنامه دیگر به روز شدند." |         "other": "{} و {} برنامه دیگر به روز شدند." | ||||||
|  |     }, | ||||||
|  |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|  |         "one": "{} and 1 more app may have been updated.", | ||||||
|  |         "other": "{} and {} more apps may have been updated." | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,12 +11,6 @@ | |||||||
|     "unexpectedError": "Erreur inattendue", |     "unexpectedError": "Erreur inattendue", | ||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "et", |     "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)", |     "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", | ||||||
|     "githubPATHint": "Le JAP doit être dans ce format : username:token", |     "githubPATHint": "Le JAP doit être dans ce format : username:token", | ||||||
|     "githubPATFormat": "username:token", |     "githubPATFormat": "username:token", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Supprimer l'application ?", |         "one": "Supprimer l'application ?", | ||||||
|         "other": "Supprimer les applications ?" |         "other": "Supprimer les applications ?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} et 1 autre application ont été mises à jour.", |         "one": "{} et 1 autre application ont été mises à jour.", | ||||||
|         "other": "{} et {} autres applications 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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Váratlan hiba", |     "unexpectedError": "Váratlan hiba", | ||||||
|     "ok": "Oké", |     "ok": "Oké", | ||||||
|     "and": "és", |     "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)", |     "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", |     "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", | ||||||
|     "githubPATFormat": "felhasználónév:token", |     "githubPATFormat": "felhasználónév:token", | ||||||
| @@ -93,7 +87,7 @@ | |||||||
|     "author": "Szerző", |     "author": "Szerző", | ||||||
|     "upToDateApps": "Naprakész appok", |     "upToDateApps": "Naprakész appok", | ||||||
|     "nonInstalledApps": "Nem telepített appok", |     "nonInstalledApps": "Nem telepített appok", | ||||||
|     "importExport": "Importálás/Exportálás", |     "importExport": "Import/Export", | ||||||
|     "settings": "Beállítások", |     "settings": "Beállítások", | ||||||
|     "exportedTo": "Exportálva ide {}", |     "exportedTo": "Exportálva ide {}", | ||||||
|     "obtainiumExport": "Obtainium Adat Exportálás", |     "obtainiumExport": "Obtainium Adat Exportálás", | ||||||
| @@ -143,7 +137,7 @@ | |||||||
|     "share": "Megosztás", |     "share": "Megosztás", | ||||||
|     "appNotFound": "App nem található", |     "appNotFound": "App nem található", | ||||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", |     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||||
|     "pickAnAPK": "Válasszon egy APK-ot", |     "pickAnAPK": "Válasszon egy APK-t", | ||||||
|     "appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:", |     "appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:", | ||||||
|     "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", |     "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", | ||||||
|     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", |     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", | ||||||
| @@ -210,7 +204,7 @@ | |||||||
|     "copiedToClipboard": "Másolva a vágólapra", |     "copiedToClipboard": "Másolva a vágólapra", | ||||||
|     "storagePermissionDenied": "Tárhely engedély megtagadva", |     "storagePermissionDenied": "Tárhely engedély megtagadva", | ||||||
|     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", |     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", | ||||||
|     "filterAPKsByRegEx": "Az APK-ok szűrése reguláris kifejezéssel", |     "filterAPKsByRegEx": "Az APK-k szűrése reguláris kifejezéssel", | ||||||
|     "removeFromObtainium": "Eltávolítás az Obtainiumból", |     "removeFromObtainium": "Eltávolítás az Obtainiumból", | ||||||
|     "uninstallFromDevice": "Eltávolítás a készülékről", |     "uninstallFromDevice": "Eltávolítás a készülékről", | ||||||
|     "onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.", |     "onlyWorksWithNonVersionDetectApps": "Csak azoknál az alkalmazásoknál működik, amelyeknél a verzióérzékelés le van tiltva.", | ||||||
| @@ -245,7 +239,16 @@ | |||||||
|     "gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.", |     "gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.", | ||||||
|     "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett", |     "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett", | ||||||
|     "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel", |     "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Eltávolítja az alkalmazást?", |         "one": "Eltávolítja az alkalmazást?", | ||||||
|         "other": "Eltávolítja az alkalmazást?" |         "other": "Eltávolítja az alkalmazást?" | ||||||
| @@ -293,5 +296,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "A(z) {} és 1 további alkalmazás frissítve.", |         "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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Errore imprevisto", |     "unexpectedError": "Errore imprevisto", | ||||||
|     "ok": "Va bene", |     "ok": "Va bene", | ||||||
|     "and": "e", |     "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)", |     "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", | ||||||
|     "githubPATHint": "PAT deve seguire questo formato: nomeutente:token", |     "githubPATHint": "PAT deve seguire questo formato: nomeutente:token", | ||||||
|     "githubPATFormat": "nomeutente:token", |     "githubPATFormat": "nomeutente:token", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", |     "gitlabSourceNote": "GitLab APK extraction may not work without an API key.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Rimuovere l'app?", |         "one": "Rimuovere l'app?", | ||||||
|         "other": "Rimuovere le app?" |         "other": "Rimuovere le app?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} e un'altra app sono state aggiornate.", |         "one": "{} e un'altra app sono state aggiornate.", | ||||||
|         "other": "{} e altre {} 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,12 @@ | |||||||
|     "unexpectedError": "予期せぬエラーが発生しました", |     "unexpectedError": "予期せぬエラーが発生しました", | ||||||
|     "ok": "OK", |     "ok": "OK", | ||||||
|     "and": "と", |     "and": "と", | ||||||
|     "startedBgUpdateTask": "バックグラウンドのアップデート確認タスクを開始", |  | ||||||
|     "bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}", |  | ||||||
|     "startedActualBGUpdateCheck": "実際のバックグラウンドのアップデート確認を開始", |  | ||||||
|     "bgUpdateTaskFinished": "バックグラウンドのアップデート確認タスクを終了", |  | ||||||
|     "firstRun": "これがObtainiumの最初の実行です", |  | ||||||
|     "settingUpdateCheckIntervalTo": "確認間隔を{}に設定する", |  | ||||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", |     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", |     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", | ||||||
|     "githubPATFormat": "ユーザー名:トークン", |     "githubPATFormat": "ユーザー名:トークン", | ||||||
|     "includePrereleases": "プレリリースを含む", |     "includePrereleases": "プレリリースを含む", | ||||||
|     "fallbackToOlderReleases": "旧リリースへのフォールバック", |     "fallbackToOlderReleases": "旧リリースへのフォールバック", | ||||||
|     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルを絞り込む", |     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", | ||||||
|     "invalidRegEx": "無効な正規表現", |     "invalidRegEx": "無効な正規表現", | ||||||
|     "noDescription": "説明はありません", |     "noDescription": "説明はありません", | ||||||
|     "cancel": "キャンセル", |     "cancel": "キャンセル", | ||||||
| @@ -88,7 +82,7 @@ | |||||||
|     "showOutdatedOnly": "アップデートが存在するアプリのみ表示する", |     "showOutdatedOnly": "アップデートが存在するアプリのみ表示する", | ||||||
|     "filter": "フィルター", |     "filter": "フィルター", | ||||||
|     "filterActive": "フィルター *", |     "filterActive": "フィルター *", | ||||||
|     "filterApps": "アプリを絞り込む", |     "filterApps": "アプリをフィルタリングする", | ||||||
|     "appName": "アプリ名", |     "appName": "アプリ名", | ||||||
|     "author": "作者", |     "author": "作者", | ||||||
|     "upToDateApps": "最新のアプリ", |     "upToDateApps": "最新のアプリ", | ||||||
| @@ -211,7 +205,7 @@ | |||||||
|     "copiedToClipboard": "クリップボードにコピーしました", |     "copiedToClipboard": "クリップボードにコピーしました", | ||||||
|     "storagePermissionDenied": "ストレージ権限が拒否されました", |     "storagePermissionDenied": "ストレージ権限が拒否されました", | ||||||
|     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", |     "selectedCategorizeWarning": "これにより、選択したアプリの既存のカテゴリ設定がすべて置き換えられます。", | ||||||
|     "filterAPKsByRegEx": "正規表現でAPKを絞り込む", |     "filterAPKsByRegEx": "正規表現でAPKをフィルタリングする", | ||||||
|     "removeFromObtainium": "Obtainiumから削除する", |     "removeFromObtainium": "Obtainiumから削除する", | ||||||
|     "uninstallFromDevice": "デバイスからアンインストールする", |     "uninstallFromDevice": "デバイスからアンインストールする", | ||||||
|     "onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。", |     "onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。", | ||||||
| @@ -244,16 +238,26 @@ | |||||||
|     "addInfoInSettings": "設定でこの情報を追加してください。", |     "addInfoInSettings": "設定でこの情報を追加してください。", | ||||||
|     "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。", |     "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。", | ||||||
|     "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。", |     "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')", | ||||||
|  |     "appsPossiblyUpdated": "アプリのアップデートを試行", | ||||||
|  |     "appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する", | ||||||
|  |     "xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります", | ||||||
|  |     "enableBackgroundUpdates": "バックグラウンドアップデートを有効化する", | ||||||
|  |     "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。", | ||||||
|  |     "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。", | ||||||
|  |     "verifyLatestTag": "'latest'タグを確認する", | ||||||
|  |     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", | ||||||
|  |     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "アプリを削除しますか?", |         "one": "アプリを削除しますか?", | ||||||
|         "other": "アプリを削除しますか?" |         "other": "アプリを削除しますか?" | ||||||
|     }, |     }, | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", |         "one": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください", | ||||||
|         "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" |         "other": "リクエストが多すぎます(レート制限)- {} 分後に再試行してください" | ||||||
|     }, |     }, | ||||||
|     "bgUpdateGotErrorRetryInMinutes": { |     "bgUpdateGotErrorRetryInMinutes": { | ||||||
|         "one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します", |         "one": "バックグラウンドでのアップデート確認で {} の問題が発生, {} 分後に再試行します", | ||||||
| @@ -264,28 +268,28 @@ | |||||||
|         "other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" |         "other": "バックグラウンドでのアップデート確認で {} 個のアップデートを発見 - 必要に応じてユーザーに通知します" | ||||||
|     }, |     }, | ||||||
|     "apps": { |     "apps": { | ||||||
|         "one": "{}個のアプリ", |         "one": "{} 個のアプリ", | ||||||
|         "other": "{}個のアプリ" |         "other": "{} 個のアプリ" | ||||||
|     }, |     }, | ||||||
|     "url": { |     "url": { | ||||||
|         "one": "{}個のURL", |         "one": "{} 個のURL", | ||||||
|         "other": "{}個のURL" |         "other": "{} 個のURL" | ||||||
|     }, |     }, | ||||||
|     "minute": { |     "minute": { | ||||||
|         "one": "{}分", |         "one": "{} 分", | ||||||
|         "other": "{}分" |         "other": "{} 分" | ||||||
|     }, |     }, | ||||||
|     "hour": { |     "hour": { | ||||||
|         "one": "{}時間", |         "one": "{} 時間", | ||||||
|         "other": "{}時間" |         "other": "{} 時間" | ||||||
|     }, |     }, | ||||||
|     "day": { |     "day": { | ||||||
|         "one": "{}日", |         "one": "{} 日", | ||||||
|         "other": "{}日" |         "other": "{} 日" | ||||||
|     }, |     }, | ||||||
|     "clearedNLogsBeforeXAfterY": { |     "clearedNLogsBeforeXAfterY": { | ||||||
|         "one": "{n}個のログをクリアしました (前 = {before}, 後 = {after})", |         "one": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})", | ||||||
|         "other": "{n}個のログをクリアしました (前 = {before}, 後 = {after})" |         "other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})" | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesAvailable": { |     "xAndNMoreUpdatesAvailable": { | ||||||
|         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です", |         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です", | ||||||
| @@ -294,5 +298,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} とさらに {} 個のアプリがアップデートされました", |         "one": "{} とさらに {} 個のアプリがアップデートされました", | ||||||
|         "other": "{} とさらに {} 個のアプリがアップデートされました" |         "other": "{} とさらに {} 個のアプリがアップデートされました" | ||||||
|  |     }, | ||||||
|  |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|  |         "one": "{} とさらに 1 個のアプリがアップデートされた可能性があります", | ||||||
|  |         "other": "{} とさらに {} 個のアプリがアップデートされた可能性があります" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,8 +4,10 @@ | |||||||
|     "okay": "Okej", |     "okay": "Okej", | ||||||
|     "appId": "ID aplikacji", |     "appId": "ID aplikacji", | ||||||
|     "bgUpdateGotErrorRetryInMinutes": { |     "bgUpdateGotErrorRetryInMinutes": { | ||||||
|         "one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min.", |         "one": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} minutę", | ||||||
|         "other": "Sprawdzanie aktualizacji w tle napotkało {}, zaplanuje ponowne sprawdzenie za {} min." |         "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 {}", |     "invalidURLForSource": "Nieprawidłowy adres URL aplikacji {}", | ||||||
|     "noReleaseFound": "Nie można znaleźć odpowiedniego wydania", |     "noReleaseFound": "Nie można znaleźć odpowiedniego wydania", | ||||||
| @@ -19,12 +21,6 @@ | |||||||
|     "unexpectedError": "Nieoczekiwany błąd", |     "unexpectedError": "Nieoczekiwany błąd", | ||||||
|     "ok": "Okej", |     "ok": "Okej", | ||||||
|     "and": "i", |     "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ń)", |     "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)", | ||||||
|     "githubPATHint": "Wymagany format: użytkownik:token", |     "githubPATHint": "Wymagany format: użytkownik:token", | ||||||
|     "githubPATFormat": "użytkownik:token", |     "githubPATFormat": "użytkownik:token", | ||||||
| @@ -250,49 +246,89 @@ | |||||||
|     "gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.", |     "gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.", | ||||||
|     "sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków", |     "sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków", | ||||||
|     "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego", |     "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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": "Auto-select highest versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Usunąć aplikację?", |         "one": "Usunąć aplikację?", | ||||||
|  |         "few": "Usunąć aplikacje?", | ||||||
|  |         "many": "Usunąć aplikacje?", | ||||||
|         "other": "Usunąć aplikacje?" |         "other": "Usunąć aplikacje?" | ||||||
|     }, |     }, | ||||||
|     "tooManyRequestsTryAgainInMinutes": { |     "tooManyRequestsTryAgainInMinutes": { | ||||||
|         "one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min.", |         "one": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} minutę", | ||||||
|         "other": "Zbyt wiele żądań (ograniczona częstotliwość) - spróbuj ponownie za {} min." |         "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": { |     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||||
|         "one": "Podczas sprawdzania aktualizacji w tle znaleziono {} aktualizację - w razie potrzeby użytkownik zostanie o tym powiadomiony", |         "one": "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" |         "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": { |     "apps": { | ||||||
|         "one": "{} aplik.", |         "one": "{} apkę", | ||||||
|         "other": "{} aplik." |         "few": "{} apki", | ||||||
|  |         "many": "{} apek", | ||||||
|  |         "other": "{} apki" | ||||||
|     }, |     }, | ||||||
|     "url": { |     "url": { | ||||||
|         "one": "{} adres URL", |         "one": "{} adres URL", | ||||||
|         "other": "{} adr. URL" |         "few": "{} adresy URL", | ||||||
|  |         "many": "{} adresów URL", | ||||||
|  |         "other": "{} adresy URL" | ||||||
|     }, |     }, | ||||||
|     "minute": { |     "minute": { | ||||||
|         "one": "{} min.", |         "one": "{} minuta", | ||||||
|         "other": "{} min." |         "few": "{} minuty", | ||||||
|  |         "many": "{} minut", | ||||||
|  |         "other": "{} minuty" | ||||||
|     }, |     }, | ||||||
|     "hour": { |     "hour": { | ||||||
|         "one": "{} godz.", |         "one": "{} godzina", | ||||||
|         "other": "{} godz." |         "few": "{} godziny", | ||||||
|  |         "many": "{} godzin", | ||||||
|  |         "other": "{} godziny" | ||||||
|     }, |     }, | ||||||
|     "day": { |     "day": { | ||||||
|         "one": "{} dzień", |         "one": "{} dzień", | ||||||
|  |         "few": "{} dni", | ||||||
|  |         "many": "{} dni", | ||||||
|         "other": "{} dni" |         "other": "{} dni" | ||||||
|     }, |     }, | ||||||
|     "clearedNLogsBeforeXAfterY": { |     "clearedNLogsBeforeXAfterY": { | ||||||
|         "one": "Wyczyszczono {n} log (przed = {before}, po = {after})", |         "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": { |     "xAndNMoreUpdatesAvailable": { | ||||||
|         "one": "{} i jeszcze 1 aplikacja mają aktualizacje.", |         "one": "{} i 1 inna apka mają aktualizacje.", | ||||||
|         "other": "{} i {} aplik. otrzymało aktualizacje." |         "few": "{} i {} inne apki mają aktualizacje.", | ||||||
|  |         "many": "{} i {} innych apek ma aktualizacje.", | ||||||
|  |         "other": "{} i {} inne apki mają aktualizacje." | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "Zaktualizowano {} i jeszcze 1 aplikację.", |         "one": "Zaktualizowano {} i 1 inną apkę.", | ||||||
|         "other": "Zaktualizowano {} i {} aplik." |         "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,12 +11,6 @@ | |||||||
|     "unexpectedError": "Неожиданная ошибка", |     "unexpectedError": "Неожиданная ошибка", | ||||||
|     "ok": "Окей", |     "ok": "Окей", | ||||||
|     "and": "и", |     "and": "и", | ||||||
|     "startedBgUpdateTask": "Запущена задача фоновой проверки обновлений", |  | ||||||
|     "bgUpdateIgnoreAfterIs": "Параметр игнорирования фоновых обновлений: {}", |  | ||||||
|     "startedActualBGUpdateCheck": "Запущена фактическая проверка фоновых обновлений", |  | ||||||
|     "bgUpdateTaskFinished": "Завершена задача фоновой проверки обновлений", |  | ||||||
|     "firstRun": "Это первый запуск Obtainium", |  | ||||||
|     "settingUpdateCheckIntervalTo": "Установка интервала проверки обновлений: {}", |  | ||||||
|     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", |     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", | ||||||
|     "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен", |     "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен", | ||||||
|     "githubPATFormat": "имя_пользователя:токен", |     "githubPATFormat": "имя_пользователя:токен", | ||||||
| @@ -246,7 +240,16 @@ | |||||||
|     "gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.", |     "gitlabSourceNote": "Извлечение APK из GitLab может не работать без ключа API.", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "Sort by file names instead of full links", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Удалить приложение?", |         "one": "Удалить приложение?", | ||||||
|         "other": "Удалить приложения?" |         "other": "Удалить приложения?" | ||||||
| @@ -294,5 +297,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} и еще 1 приложение были обновлены.", |         "one": "{} и еще 1 приложение были обновлены.", | ||||||
|         "other": "{} и еще {} приложений были обновлены." |         "other": "{} и еще {} приложений были обновлены." | ||||||
|  |     }, | ||||||
|  |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|  |         "one": "{} and 1 more app may have been updated.", | ||||||
|  |         "other": "{} and {} more apps may have been updated." | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,12 +11,6 @@ | |||||||
|     "unexpectedError": "意外错误", |     "unexpectedError": "意外错误", | ||||||
|     "ok": "好的", |     "ok": "好的", | ||||||
|     "and": "和", |     "and": "和", | ||||||
|     "startedBgUpdateTask": "后台更新检查任务已启动", |  | ||||||
|     "bgUpdateIgnoreAfterIs": "后台更新检查间隔为 {}", |  | ||||||
|     "startedActualBGUpdateCheck": "开始后台更新检查", |  | ||||||
|     "bgUpdateTaskFinished": "后台更新检查任务已完成", |  | ||||||
|     "firstRun": "这是 Obtainium 首次启动", |  | ||||||
|     "settingUpdateCheckIntervalTo": "更新检查间隔设置为 {}", |  | ||||||
|     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", |     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", | ||||||
|     "githubPATHint": "个人访问令牌必须为“username:token”的格式", |     "githubPATHint": "个人访问令牌必须为“username:token”的格式", | ||||||
|     "githubPATFormat": "username:token", |     "githubPATFormat": "username:token", | ||||||
| @@ -46,7 +40,7 @@ | |||||||
|     "appSourceURL": "来源 URL", |     "appSourceURL": "来源 URL", | ||||||
|     "error": "错误", |     "error": "错误", | ||||||
|     "add": "添加", |     "add": "添加", | ||||||
|     "searchSomeSourcesLabel": "搜索(仅部分来源)", |     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", | ||||||
|     "search": "搜索", |     "search": "搜索", | ||||||
|     "additionalOptsFor": "{} 的更多选项", |     "additionalOptsFor": "{} 的更多选项", | ||||||
|     "supportedSourcesBelow": "支持的来源:", |     "supportedSourcesBelow": "支持的来源:", | ||||||
| @@ -150,16 +144,16 @@ | |||||||
|     "warning": "警告", |     "warning": "警告", | ||||||
|     "sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?", |     "sourceIsXButPackageFromYPrompt": "此应用的来源是“{}”,但 APK 文件来自“{}”。是否继续?", | ||||||
|     "updatesAvailable": "更新可用", |     "updatesAvailable": "更新可用", | ||||||
|     "updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发出通知", |     "updatesAvailableNotifDescription": "Obtainium 追踪的应用有更新时发送通知", | ||||||
|     "noNewUpdates": "全部应用已是最新。", |     "noNewUpdates": "全部应用已是最新。", | ||||||
|     "xHasAnUpdate": "{} 可以更新了。", |     "xHasAnUpdate": "{} 可以更新了。", | ||||||
|     "appsUpdated": "应用已更新", |     "appsUpdated": "应用已更新", | ||||||
|     "appsUpdatedNotifDescription": "当应用在后台安装更新时发出通知", |     "appsUpdatedNotifDescription": "当应用在后台安装更新时发送通知", | ||||||
|     "xWasUpdatedToY": "{} 已更新至 {}。", |     "xWasUpdatedToY": "{} 已更新至 {}。", | ||||||
|     "errorCheckingUpdates": "检查更新出错", |     "errorCheckingUpdates": "检查更新出错", | ||||||
|     "errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知", |     "errorCheckingUpdatesNotifDescription": "当后台检查更新失败时显示的通知", | ||||||
|     "appsRemoved": "应用已删除", |     "appsRemoved": "应用已删除", | ||||||
|     "appsRemovedNotifDescription": "当应用因加载出错而被删除时发出通知", |     "appsRemovedNotifDescription": "当应用因加载出错而被删除时发送通知", | ||||||
|     "xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}", |     "xWasRemovedDueToErrorY": "{} 由于以下错误被删除:{}", | ||||||
|     "completeAppInstallation": "完成应用安装", |     "completeAppInstallation": "完成应用安装", | ||||||
|     "obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用", |     "obtainiumMustBeOpenToInstallApps": "必须启动 Obtainium 才能安装应用", | ||||||
| @@ -180,7 +174,7 @@ | |||||||
|     "yesMarkUpdated": "是,标记为已更新", |     "yesMarkUpdated": "是,标记为已更新", | ||||||
|     "fdroid": "F-Droid 官方存储库", |     "fdroid": "F-Droid 官方存储库", | ||||||
|     "appIdOrName": "应用 ID 或名称", |     "appIdOrName": "应用 ID 或名称", | ||||||
|     "appId": "App ID", |     "appId": "应用 ID", | ||||||
|     "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用", |     "appWithIdOrNameNotFound": "未找到符合此 ID 或名称的应用", | ||||||
|     "reposHaveMultipleApps": "存储库中可能包含多个应用", |     "reposHaveMultipleApps": "存储库中可能包含多个应用", | ||||||
|     "fdroidThirdPartyRepo": "F-Droid 第三方存储库", |     "fdroidThirdPartyRepo": "F-Droid 第三方存储库", | ||||||
| @@ -244,9 +238,19 @@ | |||||||
|     "addInfoInSettings": "在“设置”中添加此凭据。", |     "addInfoInSettings": "在“设置”中添加此凭据。", | ||||||
|     "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。", |     "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。", | ||||||
|     "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。", |     "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。", | ||||||
|     "sortByFileNamesNotLinks": "Sort by file names instead of full links", |     "sortByFileNamesNotLinks": "使用文件名代替链接进行排序", | ||||||
|     "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", |     "filterReleaseNotesByRegEx": "使用正则表达式筛选发行说明", | ||||||
|     "customLinkFilterRegex": "Custom Link Filter by Regular Expression (Default '.apk$')", |     "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", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "是否删除应用?", |         "one": "是否删除应用?", | ||||||
|         "other": "是否删除应用?" |         "other": "是否删除应用?" | ||||||
| @@ -260,8 +264,8 @@ | |||||||
|         "other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试" |         "other": "后台更新检查遇到了“{}”问题,预定于 {} 分钟后重试" | ||||||
|     }, |     }, | ||||||
|     "bgCheckFoundUpdatesWillNotifyIfNeeded": { |     "bgCheckFoundUpdatesWillNotifyIfNeeded": { | ||||||
|         "one": "后台检查发现 {} 个应用更新 - 如有需要将发出通知", |         "one": "后台检查发现 {} 个应用更新 - 如有需要将发送通知", | ||||||
|         "other": "后台检查发现 {} 个应用更新 - 如有需要将发出通知" |         "other": "后台检查发现 {} 个应用更新 - 如有需要将发送通知" | ||||||
|     }, |     }, | ||||||
|     "apps": { |     "apps": { | ||||||
|         "one": "{} 个应用", |         "one": "{} 个应用", | ||||||
| @@ -294,5 +298,9 @@ | |||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} 和另外 1 个应用已更新。", |         "one": "{} 和另外 1 个应用已更新。", | ||||||
|         "other": "{} 和另外 {} 个应用已更新。" |         "other": "{} 和另外 {} 个应用已更新。" | ||||||
|  |     }, | ||||||
|  |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|  |         "one": "{} 和另外 1 个应用已尝试更新。", | ||||||
|  |         "other": "{} 和另外 {} 个应用已尝试更新。" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										18
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # Convenience script | ||||||
|  |  | ||||||
|  | CURR_DIR="$(pwd)" | ||||||
|  | trap "cd "$CURR_DIR"" EXIT | ||||||
|  |  | ||||||
|  | git fetch && git merge origin/main && git push                                               # Typically run after a PR to main, so bring dev up to date | ||||||
|  | rm ./build/app/outputs/flutter-apk/* 2>/dev/null                                             # Get rid of older builds if any | ||||||
|  | flutter build apk && flutter build apk --split-per-abi                                       # Build (both split and combined APKs) | ||||||
|  | for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures | ||||||
|  | rsync -r ./build/app/outputs/flutter-apk/ ~/Downloads/Obtainium-build/                       # Dropoff in Downloads to allow for drag-drop into Flatpak Firefox | ||||||
|  | cd ~/Downloads/Obtainium-build/                                                              # Make zips just in case (for in-comment uploads) | ||||||
|  | for apk in *.apk; do | ||||||
|  |     PREFIX="$(echo "$apk" | head -c -5)" | ||||||
|  |     zip "$PREFIX" "$PREFIX"* | ||||||
|  | done | ||||||
|  | mkdir -p zips | ||||||
|  | mv *.zip zips/ | ||||||
							
								
								
									
										108
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import 'dart:convert'; | ||||||
|  |  | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:html/parser.dart'; | ||||||
|  | import 'package:http/http.dart'; | ||||||
|  | import 'package:obtainium/custom_errors.dart'; | ||||||
|  | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<Map<String, List<String>>> search(String query, | ||||||
|  |       {Map<String, dynamic> querySettings = const {}}) async { | ||||||
|  |     Response res = await sourceRequest( | ||||||
|  |         'https://search.$host/?q=${Uri.encodeQueryComponent(query)}'); | ||||||
|  |     if (res.statusCode == 200) { | ||||||
|  |       Map<String, List<String>> urlsWithDescriptions = {}; | ||||||
|  |       parse(res.body).querySelectorAll('.package-header').forEach((e) { | ||||||
|  |         String? url = e.attributes['href']; | ||||||
|  |         if (url != null) { | ||||||
|  |           try { | ||||||
|  |             standardizeUrl(url); | ||||||
|  |           } catch (e) { | ||||||
|  |             url = null; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (url != null) { | ||||||
|  |           urlsWithDescriptions[url] = [ | ||||||
|  |             e.querySelector('.package-name')?.text.trim() ?? '', | ||||||
|  |             e.querySelector('.package-summary')?.text.trim() ?? | ||||||
|  |                 tr('noDescription') | ||||||
|  |           ]; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       return urlsWithDescriptions; | ||||||
|  |     } else { | ||||||
|  |       throw getObtainiumHttpError(res); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ import 'dart:convert'; | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:html/parser.dart'; | import 'package:html/parser.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
|  | import 'package:obtainium/components/generated_form.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| @@ -11,6 +12,12 @@ class FDroid extends AppSource { | |||||||
|     host = 'f-droid.org'; |     host = 'f-droid.org'; | ||||||
|     name = tr('fdroid'); |     name = tr('fdroid'); | ||||||
|     canSearch = true; |     canSearch = true; | ||||||
|  |     additionalSourceAppSpecificSettingFormItems = [ | ||||||
|  |       [ | ||||||
|  |         GeneratedFormSwitch('autoSelectHighestVersionCode', | ||||||
|  |             label: tr('autoSelectHighestVersionCode')) | ||||||
|  |       ] | ||||||
|  |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -37,7 +44,8 @@ class FDroid extends AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( |   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|       Response res, String apkUrlPrefix, String standardUrl) { |       Response res, String apkUrlPrefix, String standardUrl, | ||||||
|  |       {bool autoSelectHighestVersionCode = false}) { | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; |       List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; | ||||||
|       if (releases.isEmpty) { |       if (releases.isEmpty) { | ||||||
| @@ -47,8 +55,12 @@ class FDroid extends AppSource { | |||||||
|       if (latestVersion == null) { |       if (latestVersion == null) { | ||||||
|         throw NoVersionError(); |         throw NoVersionError(); | ||||||
|       } |       } | ||||||
|       List<String> apkUrls = releases |       Iterable<dynamic> latestReleases = | ||||||
|           .where((element) => element['versionName'] == latestVersion) |           releases.where((element) => element['versionName'] == latestVersion); | ||||||
|  |       if (latestReleases.length > 1 && autoSelectHighestVersionCode) { | ||||||
|  |         latestReleases = [latestReleases.first]; | ||||||
|  |       } | ||||||
|  |       List<String> apkUrls = latestReleases | ||||||
|           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') |           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') | ||||||
|           .toList(); |           .toList(); | ||||||
|       return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), |       return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), | ||||||
| @@ -68,7 +80,9 @@ class FDroid extends AppSource { | |||||||
|     return getAPKUrlsFromFDroidPackagesAPIResponse( |     return getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|         await sourceRequest('https://$host/api/v1/packages/$appId'), |         await sourceRequest('https://$host/api/v1/packages/$appId'), | ||||||
|         'https://$host/repo/$appId', |         'https://$host/repo/$appId', | ||||||
|         standardUrl); |         standardUrl, | ||||||
|  |         autoSelectHighestVersionCode: | ||||||
|  |             additionalSettings['autoSelectHighestVersionCode'] == true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -85,6 +85,10 @@ class GitHub extends AppSource { | |||||||
|                 return regExValidator(value); |                 return regExValidator(value); | ||||||
|               } |               } | ||||||
|             ]) |             ]) | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         GeneratedFormSwitch('verifyLatestTag', | ||||||
|  |             label: tr('verifyLatestTag'), defaultValue: false) | ||||||
|       ] |       ] | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
| @@ -212,6 +216,21 @@ class GitHub extends AppSource { | |||||||
|                 true |                 true | ||||||
|             ? additionalSettings['filterReleaseNotesByRegEx'] |             ? additionalSettings['filterReleaseNotesByRegEx'] | ||||||
|             : null; |             : 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); |     Response res = await sourceRequest(requestUrl); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       var releases = jsonDecode(res.body) as List<dynamic>; |       var releases = jsonDecode(res.body) as List<dynamic>; | ||||||
| @@ -258,6 +277,17 @@ class GitHub extends AppSource { | |||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |       if (latestTag != null && | ||||||
|  |           releases.isNotEmpty && | ||||||
|  |           latestTag != | ||||||
|  |               (releases[releases.length - 1]['tag_name'] ?? | ||||||
|  |                   releases[0]['name'])) { | ||||||
|  |         var ind = releases.indexWhere( | ||||||
|  |             (element) => latestTag == (element['tag_name'] ?? element['name'])); | ||||||
|  |         if (ind >= 0) { | ||||||
|  |           releases.add(releases.removeAt(ind)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       releases = releases.reversed.toList(); |       releases = releases.reversed.toList(); | ||||||
|       dynamic targetRelease; |       dynamic targetRelease; | ||||||
|       var prerrelsSkipped = 0; |       var prerrelsSkipped = 0; | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { | |||||||
|       .toList(); |       .toList(); | ||||||
|   if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { |   if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) { | ||||||
|     return '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; |     return '${referenceAbsoluteUrl.origin}/$ambiguousUrl'; | ||||||
|   } else if (ambiguousUrl.split('/').length == 1) { |   } else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) { | ||||||
|     return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl'; |     return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl'; | ||||||
|   } else { |   } else { | ||||||
|     return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl'; |     return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl'; | ||||||
| @@ -103,7 +103,16 @@ class HTML extends AppSource { | |||||||
|               } |               } | ||||||
|             ]) |             ]) | ||||||
|       ], |       ], | ||||||
|  |       [ | ||||||
|  |         GeneratedFormTextField('intermediateLinkRegex', | ||||||
|  |             label: tr('intermediateLinkRegex'), | ||||||
|  |             hint: '([0-9]+\.)*[0-9]+/\$', | ||||||
|  |             required: false, | ||||||
|  |             additionalValidators: [(value) => regExValidator(value)]) | ||||||
|  |       ] | ||||||
|     ]; |     ]; | ||||||
|  |     overrideVersionDetectionFormDefault('noVersionDetection', | ||||||
|  |         disableStandard: true, disableRelDate: true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -132,6 +141,21 @@ class HTML extends AppSource { | |||||||
|           .map((element) => element.attributes['href'] ?? '') |           .map((element) => element.attributes['href'] ?? '') | ||||||
|           .toList(); |           .toList(); | ||||||
|       List<String> links = []; |       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?) |       if ((additionalSettings['customLinkFilterRegex'] as String?) | ||||||
|               ?.isNotEmpty == |               ?.isNotEmpty == | ||||||
|           true) { |           true) { | ||||||
|   | |||||||
							
								
								
									
										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'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| class IzzyOnDroid extends AppSource { | class IzzyOnDroid extends AppSource { | ||||||
|  |   late FDroid fd; | ||||||
|  |  | ||||||
|   IzzyOnDroid() { |   IzzyOnDroid() { | ||||||
|     host = 'android.izzysoft.de'; |     host = 'android.izzysoft.de'; | ||||||
|  |     fd = FDroid(); | ||||||
|  |     additionalSourceAppSpecificSettingFormItems = | ||||||
|  |         fd.additionalSourceAppSpecificSettingFormItems; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -20,7 +25,7 @@ class IzzyOnDroid extends AppSource { | |||||||
|   @override |   @override | ||||||
|   Future<String?> tryInferringAppId(String standardUrl, |   Future<String?> tryInferringAppId(String standardUrl, | ||||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { |       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||||
|     return FDroid().tryInferringAppId(standardUrl); |     return fd.tryInferringAppId(standardUrl); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -29,10 +34,12 @@ class IzzyOnDroid extends AppSource { | |||||||
|     Map<String, dynamic> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     String? appId = await tryInferringAppId(standardUrl); |     String? appId = await tryInferringAppId(standardUrl); | ||||||
|     return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( |     return fd.getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|         await sourceRequest( |         await sourceRequest( | ||||||
|             'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), |             'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), | ||||||
|         'https://android.izzysoft.de/frepo/$appId', |         'https://android.izzysoft.de/frepo/$appId', | ||||||
|         standardUrl); |         standardUrl, | ||||||
|  |         autoSelectHighestVersionCode: | ||||||
|  |             additionalSettings['autoSelectHighestVersionCode'] == true); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,8 @@ import 'package:obtainium/providers/source_provider.dart'; | |||||||
|  |  | ||||||
| class Jenkins extends AppSource { | class Jenkins extends AppSource { | ||||||
|   Jenkins() { |   Jenkins() { | ||||||
|     overrideVersionDetectionFormDefault('releaseDateAsVersion', true); |     overrideVersionDetectionFormDefault('releaseDateAsVersion', | ||||||
|  |         disableStandard: true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   String trimJobUrl(String url) { |   String trimJobUrl(String url) { | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:html/parser.dart'; | import 'package:html/parser.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
|  | import 'package:obtainium/app_sources/html.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| @@ -7,19 +9,41 @@ class VLC extends AppSource { | |||||||
|   VLC() { |   VLC() { | ||||||
|     host = 'videolan.org'; |     host = 'videolan.org'; | ||||||
|   } |   } | ||||||
|  |   get dwUrlBase => 'https://get.$host/vlc-android/'; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Map<String, String>? get requestHeaders => HTML().requestHeaders; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String sourceSpecificStandardizeURL(String url) { |   String sourceSpecificStandardizeURL(String url) { | ||||||
|     return 'https://$host'; |     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 |   @override | ||||||
|   Future<APKDetails> getLatestAPKDetails( |   Future<APKDetails> getLatestAPKDetails( | ||||||
|     String standardUrl, |     String standardUrl, | ||||||
|     Map<String, dynamic> additionalSettings, |     Map<String, dynamic> additionalSettings, | ||||||
|   ) async { |   ) async { | ||||||
|     Response res = await sourceRequest( |     Response res = await get( | ||||||
|         'https://www.videolan.org/vlc/download-android.html'); |         Uri.parse('https://www.videolan.org/vlc/download-android.html')); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       var dwUrlBase = 'get.videolan.org/vlc-android'; |       var dwUrlBase = 'get.videolan.org/vlc-android'; | ||||||
|       var dwLinks = parse(res.body) |       var dwLinks = parse(res.body) | ||||||
| @@ -37,20 +61,9 @@ class VLC extends AppSource { | |||||||
|         throw NoVersionError(); |         throw NoVersionError(); | ||||||
|       } |       } | ||||||
|       String? targetUrl = 'https://$dwUrlBase/$version/'; |       String? targetUrl = 'https://$dwUrlBase/$version/'; | ||||||
|       Response res2 = await sourceRequest(targetUrl); |       var apkUrls = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] | ||||||
|       List<String> apkUrls = []; |           .map((e) => '${targetUrl}VLC-Android-$version-$e.apk') | ||||||
|       if (res2.statusCode == 200) { |           .toList(); | ||||||
|         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); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return APKDetails( |       return APKDetails( | ||||||
|           version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC')); |           version, getApkUrlsFromUrls(apkUrls), AppNames('VideoLAN', 'VLC')); | ||||||
|     } else { |     } else { | ||||||
| @@ -69,6 +82,20 @@ class VLC extends AppSource { | |||||||
|         throw NoAPKError(); |         throw NoAPKError(); | ||||||
|       } |       } | ||||||
|       return apkUrl; |       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 { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,10 +1,7 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
| import 'dart:math'; |  | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.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/pages/home.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/logs_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 | // ignore: implementation_imports | ||||||
| import 'package:easy_localization/src/localization.dart'; | import 'package:easy_localization/src/localization.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.13.24'; | const String currentVersion = '0.14.10'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
| @@ -41,6 +38,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [ | |||||||
|   MapEntry(Locale('pl'), 'Polski'), |   MapEntry(Locale('pl'), 'Polski'), | ||||||
|   MapEntry(Locale('ru'), 'Русский язык'), |   MapEntry(Locale('ru'), 'Русский язык'), | ||||||
|   MapEntry(Locale('bs'), 'Bosanski'), |   MapEntry(Locale('bs'), 'Bosanski'), | ||||||
|  |   // MapEntry(Locale('br'), 'Brasileiro'), | ||||||
| ]; | ]; | ||||||
| const fallbackLocale = Locale('en'); | const fallbackLocale = Locale('en'); | ||||||
| const localeDir = 'assets/translations'; | const localeDir = 'assets/translations'; | ||||||
| @@ -72,89 +70,6 @@ Future<void> loadTranslations() async { | |||||||
|       fallbackTranslations: controller.fallbackTranslations); |       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 { | void main() async { | ||||||
|   WidgetsFlutterBinding.ensureInitialized(); |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
|   try { |   try { | ||||||
| @@ -212,7 +127,7 @@ class _ObtainiumState extends State<Obtainium> { | |||||||
|     } else { |     } else { | ||||||
|       bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); |       bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); | ||||||
|       if (isFirstRun) { |       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 |         // If this is the first run, ask for notification permissions and add Obtainium to the Apps list | ||||||
|         Permission.notification.request(); |         Permission.notification.request(); | ||||||
|         appsProvider.saveApps([ |         appsProvider.saveApps([ | ||||||
| @@ -239,22 +154,28 @@ class _ObtainiumState extends State<Obtainium> { | |||||||
|         settingsProvider.resetLocaleSafe(context); |         settingsProvider.resetLocaleSafe(context); | ||||||
|       } |       } | ||||||
|       // Register the background update task according to the user's setting |       // Register the background update task according to the user's setting | ||||||
|       if (existingUpdateInterval != settingsProvider.updateInterval) { |       var actualUpdateInterval = settingsProvider.updateInterval; | ||||||
|         if (existingUpdateInterval != -1) { |       if (existingUpdateInterval != actualUpdateInterval) { | ||||||
|           logs.add(tr('settingUpdateCheckIntervalTo', |         if (actualUpdateInterval == 0) { | ||||||
|               args: [settingsProvider.updateInterval.toString()])); |  | ||||||
|         } |  | ||||||
|         existingUpdateInterval = settingsProvider.updateInterval; |  | ||||||
|         if (existingUpdateInterval == 0) { |  | ||||||
|           AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); |           AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); | ||||||
|         } else { |         } else { | ||||||
|           AndroidAlarmManager.periodic( |           var settingChanged = existingUpdateInterval != -1; | ||||||
|               Duration(minutes: existingUpdateInterval), |           var lastCheckWasTooLongAgo = actualUpdateInterval != 0 && | ||||||
|               bgUpdateCheckAlarmId, |               settingsProvider.lastBGCheckTime | ||||||
|               bgUpdateCheck, |                   .add(Duration(minutes: actualUpdateInterval + 60)) | ||||||
|               rescheduleOnReboot: true, |                   .isBefore(DateTime.now()); | ||||||
|               wakeup: true); |           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 |                   ? lightColorScheme | ||||||
|                   : darkColorScheme, |                   : darkColorScheme, | ||||||
|               fontFamily: 'Metropolis'), |               fontFamily: 'Metropolis'), | ||||||
|           home: const HomePage()); |           home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{ | ||||||
|  |             LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), | ||||||
|  |           }, child: const HomePage())); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -153,10 +153,10 @@ class _AppPageState extends State<AppPage> { | |||||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|           children: [ |           children: [ | ||||||
|             const SizedBox(height: 125), |             const SizedBox(height: 125), | ||||||
|             app?.installedInfo != null |             app?.icon != null | ||||||
|                 ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ |                 ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ | ||||||
|                     Image.memory( |                     Image.memory( | ||||||
|                       app!.installedInfo!.icon!, |                       app!.icon!, | ||||||
|                       height: 150, |                       height: 150, | ||||||
|                       gaplessPlayback: true, |                       gaplessPlayback: true, | ||||||
|                     ) |                     ) | ||||||
| @@ -339,11 +339,17 @@ class _AppPageState extends State<AppPage> { | |||||||
|                   HapticFeedback.heavyImpact(); |                   HapticFeedback.heavyImpact(); | ||||||
|                   var res = await appsProvider.downloadAndInstallLatestApps( |                   var res = await appsProvider.downloadAndInstallLatestApps( | ||||||
|                       app?.app.id != null ? [app!.app.id] : [], |                       app?.app.id != null ? [app!.app.id] : [], | ||||||
|                       globalNavigatorKey.currentContext); |                       globalNavigatorKey.currentContext, | ||||||
|  |                       settingsProvider); | ||||||
|  |                   if (app?.app.installedVersion != null && !trackOnly) { | ||||||
|  |                     // ignore: use_build_context_synchronously | ||||||
|  |                     showError(tr('appsUpdated'), context); | ||||||
|  |                   } | ||||||
|                   if (res.isNotEmpty && mounted) { |                   if (res.isNotEmpty && mounted) { | ||||||
|                     Navigator.of(context).pop(); |                     Navigator.of(context).pop(); | ||||||
|                   } |                   } | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|  |                   // ignore: use_build_context_synchronously | ||||||
|                   showError(e, context); |                   showError(e, context); | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|   | |||||||
| @@ -381,7 +381,8 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               : () { |               : () { | ||||||
|                   appsProvider.downloadAndInstallLatestApps( |                   appsProvider.downloadAndInstallLatestApps( | ||||||
|                       [listedApps[appIndex].app.id], |                       [listedApps[appIndex].app.id], | ||||||
|                       globalNavigatorKey.currentContext).catchError((e) { |                       globalNavigatorKey.currentContext, | ||||||
|  |                       settingsProvider).catchError((e) { | ||||||
|                     showError(e, context); |                     showError(e, context); | ||||||
|                     return <String>[]; |                     return <String>[]; | ||||||
|                   }); |                   }); | ||||||
| @@ -393,9 +394,9 @@ class AppsPageState extends State<AppsPage> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     getAppIcon(int appIndex) { |     getAppIcon(int appIndex) { | ||||||
|       return listedApps[appIndex].installedInfo != null |       return listedApps[appIndex].icon != null | ||||||
|           ? Image.memory( |           ? Image.memory( | ||||||
|               listedApps[appIndex].installedInfo!.icon!, |               listedApps[appIndex].icon!, | ||||||
|               gaplessPlayback: true, |               gaplessPlayback: true, | ||||||
|             ) |             ) | ||||||
|           : Row( |           : Row( | ||||||
| @@ -643,15 +644,15 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     label: tr('installX', args: [ |                     label: tr('installX', args: [ | ||||||
|                       plural('apps', newInstallIdsAllOrSelected.length) |                       plural('apps', newInstallIdsAllOrSelected.length) | ||||||
|                     ]), |                     ]), | ||||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty)); |                     defaultValue: existingUpdateIdsAllOrSelected.isEmpty)); | ||||||
|               } |               } | ||||||
|               if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { |               if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) { | ||||||
|                 formItems.add(GeneratedFormSwitch('trackonlies', |                 formItems.add(GeneratedFormSwitch('trackonlies', | ||||||
|                     label: tr('markXTrackOnlyAsUpdated', args: [ |                     label: tr('markXTrackOnlyAsUpdated', args: [ | ||||||
|                       plural('apps', trackOnlyUpdateIdsAllOrSelected.length) |                       plural('apps', trackOnlyUpdateIdsAllOrSelected.length) | ||||||
|                     ]), |                     ]), | ||||||
|                     defaultValue: existingUpdateIdsAllOrSelected.isNotEmpty || |                     defaultValue: existingUpdateIdsAllOrSelected.isEmpty && | ||||||
|                         newInstallIdsAllOrSelected.isNotEmpty)); |                         newInstallIdsAllOrSelected.isEmpty)); | ||||||
|               } |               } | ||||||
|               showDialog<Map<String, dynamic>?>( |               showDialog<Map<String, dynamic>?>( | ||||||
|                   context: context, |                   context: context, | ||||||
| @@ -683,12 +684,15 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); |                     toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); | ||||||
|                   } |                   } | ||||||
|                   appsProvider |                   appsProvider | ||||||
|                       .downloadAndInstallLatestApps( |                       .downloadAndInstallLatestApps(toInstall, | ||||||
|                           toInstall, globalNavigatorKey.currentContext, |                           globalNavigatorKey.currentContext, settingsProvider) | ||||||
|                           settingsProvider: settingsProvider) |  | ||||||
|                       .catchError((e) { |                       .catchError((e) { | ||||||
|                     showError(e, context); |                     showError(e, context); | ||||||
|                     return <String>[]; |                     return <String>[]; | ||||||
|  |                   }).then((value) { | ||||||
|  |                     if (shouldInstallUpdates) { | ||||||
|  |                       showError(tr('appsUpdated'), context); | ||||||
|  |                     } | ||||||
|                   }); |                   }); | ||||||
|                 } |                 } | ||||||
|               }); |               }); | ||||||
|   | |||||||
| @@ -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:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:obtainium/components/custom_app_bar.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( |     const height16 = SizedBox( | ||||||
|       height: 16, |       height: 16, | ||||||
|     ); |     ); | ||||||
| @@ -211,6 +217,72 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                                   color: Theme.of(context).colorScheme.primary), |                                   color: Theme.of(context).colorScheme.primary), | ||||||
|                             ), |                             ), | ||||||
|                             intervalDropdown, |                             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, |                             height16, | ||||||
|                             Row( |                             Row( | ||||||
|                               mainAxisAlignment: MainAxisAlignment.spaceBetween, |                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
| @@ -462,7 +534,44 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                         label: Text(tr('appLogs'))), |                         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,24 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  | import 'dart:math'; | ||||||
|  |  | ||||||
|  | import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | ||||||
| import 'package:android_intent_plus/flag.dart'; | import 'package:android_intent_plus/flag.dart'; | ||||||
| import 'package:android_package_installer/android_package_installer.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:device_info_plus/device_info_plus.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.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.dart'; | ||||||
| import 'package:obtainium/components/generated_form_modal.dart'; | import 'package:obtainium/components/generated_form_modal.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
|  | import 'package:obtainium/main.dart'; | ||||||
| import 'package:obtainium/providers/logs_provider.dart'; | import 'package:obtainium/providers/logs_provider.dart'; | ||||||
| import 'package:obtainium/providers/notifications_provider.dart'; | import 'package:obtainium/providers/notifications_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:package_archive_info/package_archive_info.dart'; |  | ||||||
| import 'package:permission_handler/permission_handler.dart'; | import 'package:permission_handler/permission_handler.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:path_provider/path_provider.dart'; | import 'package:path_provider/path_provider.dart'; | ||||||
| @@ -29,16 +31,19 @@ import 'package:http/http.dart'; | |||||||
| import 'package:android_intent_plus/android_intent.dart'; | import 'package:android_intent_plus/android_intent.dart'; | ||||||
| import 'package:flutter_archive/flutter_archive.dart'; | import 'package:flutter_archive/flutter_archive.dart'; | ||||||
|  |  | ||||||
|  | final pm = AndroidPackageManager(); | ||||||
|  |  | ||||||
| class AppInMemory { | class AppInMemory { | ||||||
|   late App app; |   late App app; | ||||||
|   double? downloadProgress; |   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 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 { | class DownloadedApk { | ||||||
| @@ -97,6 +102,41 @@ Set<String> findStandardFormatsForVersion(String version, bool strict) { | |||||||
|   return results; |   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 { | class AppsProvider with ChangeNotifier { | ||||||
|   // In memory App state (should always be kept in sync with local storage versions) |   // In memory App state (should always be kept in sync with local storage versions) | ||||||
|   Map<String, AppInMemory> apps = {}; |   Map<String, AppInMemory> apps = {}; | ||||||
| @@ -112,7 +152,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|  |  | ||||||
|   Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); |   Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); | ||||||
|  |  | ||||||
|   AppsProvider() { |   AppsProvider({isBg = false}) { | ||||||
|     // Subscribe to changes in the app foreground status |     // Subscribe to changes in the app foreground status | ||||||
|     foregroundStream = FGBGEvents.stream.asBroadcastStream(); |     foregroundStream = FGBGEvents.stream.asBroadcastStream(); | ||||||
|     foregroundSubscription = foregroundStream?.listen((event) async { |     foregroundSubscription = foregroundStream?.listen((event) async { | ||||||
| @@ -130,17 +170,21 @@ class AppsProvider with ChangeNotifier { | |||||||
|           APKDir.createSync(); |           APKDir.createSync(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       // Load Apps into memory (in background, this is done later instead of in the constructor) |       if (!isBg) { | ||||||
|       await loadApps(); |         // Load Apps into memory (in background processes, this is done later instead of in the constructor) | ||||||
|       // Delete any partial APKs |         await loadApps(); | ||||||
|       var cutoff = DateTime.now().subtract(const Duration(days: 7)); |         // Delete any partial APKs (if safe to do so) | ||||||
|       APKDir.listSync() |         var cutoff = DateTime.now().subtract(const Duration(days: 7)); | ||||||
|           .where((element) => |         APKDir.listSync() | ||||||
|               element.path.endsWith('.part') || |             .where((element) => | ||||||
|               element.statSync().modified.isBefore(cutoff)) |                 element.path.endsWith('.part') || | ||||||
|           .forEach((partialApk) { |                 element.statSync().modified.isBefore(cutoff)) | ||||||
|         partialApk.delete(recursive: true); |             .forEach((partialApk) { | ||||||
|       }); |           if (!areDownloadsRunning()) { | ||||||
|  |             partialApk.delete(recursive: true); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|     }(); |     }(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -215,19 +259,19 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return downloadedFile; |     return downloadedFile; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<File> handleAPKIDChange(App app, PackageArchiveInfo newInfo, |   Future<File> handleAPKIDChange(App app, PackageInfo newInfo, | ||||||
|       File downloadedFile, String downloadUrl) async { |       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 |     // 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 |     // The former case should be handled (give the App its real ID), the latter is a security issue | ||||||
|     if (app.id != newInfo.packageName) { |     if (app.id != newInfo.packageName) { | ||||||
|       var isTempId = SourceProvider().isTempId(app); |       var isTempId = SourceProvider().isTempId(app); | ||||||
|       if (apps[app.id] != null && !isTempId && !app.allowIdChange) { |       if (apps[app.id] != null && !isTempId && !app.allowIdChange) { | ||||||
|         throw IDChangedError(newInfo.packageName); |         throw IDChangedError(newInfo.packageName!); | ||||||
|       } |       } | ||||||
|       var idChangeWasAllowed = app.allowIdChange; |       var idChangeWasAllowed = app.allowIdChange; | ||||||
|       app.allowIdChange = false; |       app.allowIdChange = false; | ||||||
|       var originalAppId = app.id; |       var originalAppId = app.id; | ||||||
|       app.id = newInfo.packageName; |       app.id = newInfo.packageName!; | ||||||
|       downloadedFile = downloadedFile.renameSync( |       downloadedFile = downloadedFile.renameSync( | ||||||
|           '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); |           '${downloadedFile.parent.path}/${app.id}-${downloadUrl.hashCode}.${downloadedFile.path.split('.').last}'); | ||||||
|       if (apps[originalAppId] != null) { |       if (apps[originalAppId] != null) { | ||||||
| @@ -238,9 +282,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return downloadedFile; |     return downloadedFile; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<Object> downloadApp(App app, BuildContext? context) async { |   Future<Object> downloadApp(App app, BuildContext? context, | ||||||
|     NotificationsProvider? notificationsProvider = |       {NotificationsProvider? notificationsProvider}) async { | ||||||
|         context?.read<NotificationsProvider>(); |  | ||||||
|     var notifId = DownloadNotification(app.finalName, 0).id; |     var notifId = DownloadNotification(app.finalName, 0).id; | ||||||
|     if (apps[app.id] != null) { |     if (apps[app.id] != null) { | ||||||
|       apps[app.id]!.downloadProgress = 0; |       apps[app.id]!.downloadProgress = 0; | ||||||
| @@ -276,11 +319,12 @@ class AppsProvider with ChangeNotifier { | |||||||
|         notif = DownloadNotification(app.finalName, -1); |         notif = DownloadNotification(app.finalName, -1); | ||||||
|         notificationsProvider?.notify(notif); |         notificationsProvider?.notify(notif); | ||||||
|       } |       } | ||||||
|       PackageArchiveInfo? newInfo; |       PackageInfo? newInfo; | ||||||
|       var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk'); |       var isAPK = downloadedFile.path.toLowerCase().endsWith('.apk'); | ||||||
|       Directory? xapkDir; |       Directory? xapkDir; | ||||||
|       if (isAPK) { |       if (isAPK) { | ||||||
|         newInfo = await PackageArchiveInfo.fromPath(downloadedFile.path); |         newInfo = await pm.getPackageArchiveInfo( | ||||||
|  |             archiveFilePath: downloadedFile.path); | ||||||
|       } else { |       } else { | ||||||
|         // Assume XAPK |         // Assume XAPK | ||||||
|         String xapkDirPath = '${downloadedFile.path}-dir'; |         String xapkDirPath = '${downloadedFile.path}-dir'; | ||||||
| @@ -290,10 +334,11 @@ class AppsProvider with ChangeNotifier { | |||||||
|             .listSync() |             .listSync() | ||||||
|             .where((e) => e.path.toLowerCase().endsWith('.apk')) |             .where((e) => e.path.toLowerCase().endsWith('.apk')) | ||||||
|             .toList(); |             .toList(); | ||||||
|         newInfo = await PackageArchiveInfo.fromPath(apks.first.path); |         newInfo = | ||||||
|  |             await pm.getPackageArchiveInfo(archiveFilePath: apks.first.path); | ||||||
|       } |       } | ||||||
|       downloadedFile = |       downloadedFile = | ||||||
|           await handleAPKIDChange(app, newInfo, downloadedFile, downloadUrl); |           await handleAPKIDChange(app, newInfo!, downloadedFile, downloadUrl); | ||||||
|       // Delete older versions of the file if any |       // Delete older versions of the file if any | ||||||
|       for (var file in downloadedFile.parent.listSync()) { |       for (var file in downloadedFile.parent.listSync()) { | ||||||
|         var fn = file.path.split('/').last; |         var fn = file.path.split('/').last; | ||||||
| @@ -321,17 +366,44 @@ class AppsProvider with ChangeNotifier { | |||||||
|       .where((element) => element.downloadProgress != null) |       .where((element) => element.downloadProgress != null) | ||||||
|       .isNotEmpty; |       .isNotEmpty; | ||||||
|  |  | ||||||
|   Future<bool> canInstallSilently(App app) async { |   Future<bool> canInstallSilently( | ||||||
|     return false; |       App app, SettingsProvider settingsProvider) async { | ||||||
|     // TODO: Uncomment the below if silent updates are ever figured out |     if (app.id == obtainiumId) { | ||||||
|     // // NOTE: This is unreliable - try to get from OS in the future |       return false; | ||||||
|     // if (app.apkUrls.length > 1) { |     } | ||||||
|     //    return false; |     if (!settingsProvider.enableBackgroundUpdates) { | ||||||
|     // } |       return false; | ||||||
|     // var osInfo = await DeviceInfoPlugin().androidInfo; |     } | ||||||
|     // return app.installedVersion != null && |     if (app.additionalSettings['exemptFromBackgroundUpdates'] == true) { | ||||||
|     //     osInfo.version.sdkInt >= 30 && |       return false; | ||||||
|     //     osInfo.version.release.compareTo('12') >= 0; |     } | ||||||
|  |     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 { |   Future<void> waitForUserToReturnToForeground(BuildContext context) async { | ||||||
| @@ -345,40 +417,32 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> canDowngradeApps() async { |   Future<bool> canDowngradeApps() async => | ||||||
|     try { |       (await getInstalledInfo('com.berdik.letmedowngrade')) != null; | ||||||
|       await InstalledApps.getAppInfo('com.berdik.letmedowngrade'); |  | ||||||
|       return true; |  | ||||||
|     } catch (e) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<void> unzipFile(String filePath, String destinationPath) async { |   Future<void> unzipFile(String filePath, String destinationPath) async { | ||||||
|     await ZipFile.extractToDirectory( |     await ZipFile.extractToDirectory( | ||||||
|         zipFile: File(filePath), destinationDir: Directory(destinationPath)); |         zipFile: File(filePath), destinationDir: Directory(destinationPath)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> installXApkDir(DownloadedXApkDir dir, |   Future<void> installXApkDir(DownloadedXApkDir dir) async { | ||||||
|       {bool silent = false}) async { |  | ||||||
|     // We don't know which APKs in an XAPK are supported by the user's device |     // 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 |     // 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 |     // If 0 APKs installed, throw the first install error encountered | ||||||
|     try { |     try { | ||||||
|       var somethingInstalled = false; |       var somethingInstalled = false; | ||||||
|       Object? firstError; |       MultiAppMultiError errors = MultiAppMultiError(); | ||||||
|       for (var file in dir.extracted |       for (var file in dir.extracted | ||||||
|           .listSync(recursive: true, followLinks: false) |           .listSync(recursive: true, followLinks: false) | ||||||
|           .whereType<File>()) { |           .whereType<File>()) { | ||||||
|         if (file.path.toLowerCase().endsWith('.apk')) { |         if (file.path.toLowerCase().endsWith('.apk')) { | ||||||
|           try { |           try { | ||||||
|             somethingInstalled = somethingInstalled || |             somethingInstalled = somethingInstalled || | ||||||
|                 await installApk(DownloadedApk(dir.appId, file), |                 await installApk(DownloadedApk(dir.appId, file)); | ||||||
|                     silent: silent); |  | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             logs.add( |             logs.add( | ||||||
|                 'Could not install APK from XAPK \'${file.path}\': ${e.toString()}'); |                 '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')) { |         } else if (file.path.toLowerCase().endsWith('.obb')) { | ||||||
|           await moveObbFile(file, dir.appId); |           await moveObbFile(file, dir.appId); | ||||||
| @@ -386,25 +450,20 @@ class AppsProvider with ChangeNotifier { | |||||||
|       } |       } | ||||||
|       if (somethingInstalled) { |       if (somethingInstalled) { | ||||||
|         dir.file.delete(recursive: true); |         dir.file.delete(recursive: true); | ||||||
|       } else if (firstError != null) { |       } else if (errors.content.isNotEmpty) { | ||||||
|         throw firstError; |         throw errors; | ||||||
|       } |       } | ||||||
|     } finally { |     } finally { | ||||||
|       dir.extracted.delete(recursive: true); |       dir.extracted.delete(recursive: true); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> installApk(DownloadedApk file, {bool silent = false}) async { |   Future<bool> installApk(DownloadedApk file) async { | ||||||
|     // TODO: Use 'silent' when/if ever possible |     var newInfo = | ||||||
|     var newInfo = await PackageArchiveInfo.fromPath(file.file.path); |         await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); | ||||||
|     AppInfo? appInfo; |     PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); | ||||||
|     try { |  | ||||||
|       appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id); |  | ||||||
|     } catch (e) { |  | ||||||
|       // OK |  | ||||||
|     } |  | ||||||
|     if (appInfo != null && |     if (appInfo != null && | ||||||
|         int.parse(newInfo.buildNumber) < appInfo.versionCode! && |         newInfo!.versionCode! < appInfo.versionCode! && | ||||||
|         !(await canDowngradeApps())) { |         !(await canDowngradeApps())) { | ||||||
|       throw DowngradeError(); |       throw DowngradeError(); | ||||||
|     } |     } | ||||||
| @@ -498,9 +557,11 @@ class AppsProvider with ChangeNotifier { | |||||||
|   // If no BuildContext is provided, apps that require user interaction are ignored |   // If no BuildContext is provided, apps that require user interaction are ignored | ||||||
|   // If user input is needed and the App is in the background, a notification is sent to get the user's attention |   // If user input is needed and the App is in the background, a notification is sent to get the user's attention | ||||||
|   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result |   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result | ||||||
|   Future<List<String>> downloadAndInstallLatestApps( |   Future<List<String>> downloadAndInstallLatestApps(List<String> appIds, | ||||||
|       List<String> appIds, BuildContext? context, |       BuildContext? context, SettingsProvider settingsProvider, | ||||||
|       {SettingsProvider? settingsProvider}) async { |       {NotificationsProvider? notificationsProvider}) async { | ||||||
|  |     notificationsProvider = | ||||||
|  |         notificationsProvider ?? context?.read<NotificationsProvider>(); | ||||||
|     List<String> appsToInstall = []; |     List<String> appsToInstall = []; | ||||||
|     List<String> trackOnlyAppsToUpdate = []; |     List<String> trackOnlyAppsToUpdate = []; | ||||||
|     // For all specified Apps, filter out those for which: |     // For all specified Apps, filter out those for which: | ||||||
| @@ -526,7 +587,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|           apps[id]!.app.preferredApkIndex = urlInd; |           apps[id]!.app.preferredApkIndex = urlInd; | ||||||
|           await saveApps([apps[id]!.app]); |           await saveApps([apps[id]!.app]); | ||||||
|         } |         } | ||||||
|         if (context != null || await canInstallSilently(apps[id]!.app)) { |         if (context != null || | ||||||
|  |             await canInstallSilently(apps[id]!.app, settingsProvider)) { | ||||||
|           appsToInstall.add(id); |           appsToInstall.add(id); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -546,22 +608,15 @@ class AppsProvider with ChangeNotifier { | |||||||
|     List<String> installedIds = []; |     List<String> installedIds = []; | ||||||
|  |  | ||||||
|     // Move Obtainium to the end of the line (let all other apps update first) |     // Move Obtainium to the end of the line (let all other apps update first) | ||||||
|     String? temp; |     appsToInstall = | ||||||
|     appsToInstall.removeWhere((element) { |         moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId); | ||||||
|       bool res = element == obtainiumId || element == obtainiumTempId; |  | ||||||
|       if (res) { |  | ||||||
|         temp = element; |  | ||||||
|       } |  | ||||||
|       return res; |  | ||||||
|     }); |  | ||||||
|     if (temp != null) { |  | ||||||
|       appsToInstall = [...appsToInstall, temp!]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     for (var id in appsToInstall) { |     for (var id in appsToInstall) { | ||||||
|       try { |       try { | ||||||
|         // ignore: use_build_context_synchronously |         var downloadedArtifact = | ||||||
|         var downloadedArtifact = await downloadApp(apps[id]!.app, context); |             // ignore: use_build_context_synchronously | ||||||
|  |             await downloadApp(apps[id]!.app, context, | ||||||
|  |                 notificationsProvider: notificationsProvider); | ||||||
|         DownloadedApk? downloadedFile; |         DownloadedApk? downloadedFile; | ||||||
|         DownloadedXApkDir? downloadedDir; |         DownloadedXApkDir? downloadedDir; | ||||||
|         if (downloadedArtifact is DownloadedApk) { |         if (downloadedArtifact is DownloadedApk) { | ||||||
| @@ -569,11 +624,10 @@ class AppsProvider with ChangeNotifier { | |||||||
|         } else { |         } else { | ||||||
|           downloadedDir = downloadedArtifact as DownloadedXApkDir; |           downloadedDir = downloadedArtifact as DownloadedXApkDir; | ||||||
|         } |         } | ||||||
|         bool willBeSilent = await canInstallSilently( |         var appId = downloadedFile?.appId ?? downloadedDir!.appId; | ||||||
|             apps[downloadedFile?.appId ?? downloadedDir!.appId]!.app); |         bool willBeSilent = | ||||||
|         willBeSilent = false; // TODO: Remove this when silent updates work |             await canInstallSilently(apps[appId]!.app, settingsProvider); | ||||||
|         if (!(await settingsProvider?.getInstallPermission(enforce: false) ?? |         if (!(await settingsProvider.getInstallPermission(enforce: false))) { | ||||||
|             true)) { |  | ||||||
|           throw ObtainiumError(tr('cancelled')); |           throw ObtainiumError(tr('cancelled')); | ||||||
|         } |         } | ||||||
|         if (!willBeSilent && context != null) { |         if (!willBeSilent && context != null) { | ||||||
| @@ -584,9 +638,24 @@ class AppsProvider with ChangeNotifier { | |||||||
|         notifyListeners(); |         notifyListeners(); | ||||||
|         try { |         try { | ||||||
|           if (downloadedFile != null) { |           if (downloadedFile != null) { | ||||||
|             await installApk(downloadedFile, silent: willBeSilent); |             if (willBeSilent && context == null) { | ||||||
|  |               // Would await forever - workaround - TODO | ||||||
|  |               installApk(downloadedFile); | ||||||
|  |             } else { | ||||||
|  |               await installApk(downloadedFile); | ||||||
|  |             } | ||||||
|           } else { |           } else { | ||||||
|             await installXApkDir(downloadedDir!, silent: willBeSilent); |             if (willBeSilent && context == null) { | ||||||
|  |               // Would await forever - workaround - TODO | ||||||
|  |               installXApkDir(downloadedDir!); | ||||||
|  |             } else { | ||||||
|  |               await installXApkDir(downloadedDir!); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           if (willBeSilent && context == null) { | ||||||
|  |             notificationsProvider?.notify(SilentUpdateAttemptNotification( | ||||||
|  |                 [apps[appId]!.app], | ||||||
|  |                 id: appId.hashCode)); | ||||||
|           } |           } | ||||||
|         } finally { |         } finally { | ||||||
|           apps[id]?.downloadProgress = null; |           apps[id]?.downloadProgress = null; | ||||||
| @@ -602,8 +671,6 @@ class AppsProvider with ChangeNotifier { | |||||||
|       throw errors; |       throw errors; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     NotificationsProvider().cancel(UpdateNotification([]).id); |  | ||||||
|  |  | ||||||
|     return installedIds; |     return installedIds; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -616,27 +683,17 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return appsDir; |     return appsDir; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<AppInfo?> getInstalledInfo(String? packageName) async { |   Future<PackageInfo?> getInstalledInfo(String? packageName) async { | ||||||
|     if (packageName != null) { |     if (packageName != null) { | ||||||
|       try { |       try { | ||||||
|         return await InstalledApps.getAppInfo(packageName); |         return await pm.getPackageInfo(packageName: packageName); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         // OK |         print(e); // OK | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return null; |     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) { |   bool isVersionDetectionPossible(AppInMemory? app) { | ||||||
|     return app?.app.additionalSettings['trackOnly'] != true && |     return app?.app.additionalSettings['trackOnly'] != true && | ||||||
|         app?.app.additionalSettings['versionDetection'] != |         app?.app.additionalSettings['versionDetection'] != | ||||||
| @@ -650,7 +707,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|  |  | ||||||
|   // Given an App and it's on-device info... |   // Given an App and it's on-device info... | ||||||
|   // Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version |   // 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 modded = false; | ||||||
|     var trackOnly = app.additionalSettings['trackOnly'] == true; |     var trackOnly = app.additionalSettings['trackOnly'] == true; | ||||||
|     var noVersionDetection = app.additionalSettings['versionDetection'] != |     var noVersionDetection = app.additionalSettings['versionDetection'] != | ||||||
| @@ -696,22 +754,12 @@ class AppsProvider with ChangeNotifier { | |||||||
|     if (installedInfo != null && |     if (installedInfo != null && | ||||||
|         app.additionalSettings['versionDetection'] == |         app.additionalSettings['versionDetection'] == | ||||||
|             'standardVersionDetection' && |             'standardVersionDetection' && | ||||||
|         !isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) { |         !isVersionDetectionPossible( | ||||||
|  |             AppInMemory(app, null, installedInfo, null))) { | ||||||
|       app.additionalSettings['versionDetection'] = 'noVersionDetection'; |       app.additionalSettings['versionDetection'] = 'noVersionDetection'; | ||||||
|       logs.add('Could not reconcile version formats for: ${app.id}'); |       logs.add('Could not reconcile version formats for: ${app.id}'); | ||||||
|       modded = true; |       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; |     return modded ? app : null; | ||||||
|   } |   } | ||||||
| @@ -750,7 +798,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|         : false; |         : false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> loadApps() async { |   Future<void> loadApps({String? singleId}) async { | ||||||
|     while (loadingApps) { |     while (loadingApps) { | ||||||
|       await Future.delayed(const Duration(microseconds: 1)); |       await Future.delayed(const Duration(microseconds: 1)); | ||||||
|     } |     } | ||||||
| @@ -761,6 +809,10 @@ class AppsProvider with ChangeNotifier { | |||||||
|     List<App?> newApps = (await getAppsDir()) // Parse Apps from JSON |     List<App?> newApps = (await getAppsDir()) // Parse Apps from JSON | ||||||
|         .listSync() |         .listSync() | ||||||
|         .where((item) => item.path.toLowerCase().endsWith('.json')) |         .where((item) => item.path.toLowerCase().endsWith('.json')) | ||||||
|  |         .where((item) => | ||||||
|  |             singleId == null || | ||||||
|  |             item.path.split('/').last.toLowerCase() == | ||||||
|  |                 '${singleId.toLowerCase()}.json') | ||||||
|         .map((e) { |         .map((e) { | ||||||
|       try { |       try { | ||||||
|         return App.fromJson(jsonDecode(File(e.path).readAsStringSync())); |         return App.fromJson(jsonDecode(File(e.path).readAsStringSync())); | ||||||
| @@ -780,9 +832,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|           sp.getSource(app.url, overrideSource: app.overrideSource); |           sp.getSource(app.url, overrideSource: app.overrideSource); | ||||||
|           apps.update( |           apps.update( | ||||||
|               app.id, |               app.id, | ||||||
|               (value) => |               (value) => AppInMemory( | ||||||
|                   AppInMemory(app, value.downloadProgress, value.installedInfo), |                   app, value.downloadProgress, value.installedInfo, value.icon), | ||||||
|               ifAbsent: () => AppInMemory(app, null, null)); |               ifAbsent: () => AppInMemory(app, null, null, null)); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|           errors.add([app.id, app.finalName, e.toString()]); |           errors.add([app.id, app.finalName, e.toString()]); | ||||||
|         } |         } | ||||||
| @@ -795,34 +847,39 @@ class AppsProvider with ChangeNotifier { | |||||||
|           AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); |           AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (await doesInstalledAppsPluginWork()) { |     for (var app in apps.values) { | ||||||
|       for (var app in apps.values) { |       // Get install status and other OS info for each App (slow) | ||||||
|         // Check install status for each App (slow) |       apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id); | ||||||
|         apps[app.app.id]?.installedInfo = await getInstalledInfo(app.app.id); |       apps[app.app.id]?.icon = | ||||||
|         notifyListeners(); |           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 = []; |     if (modifiedApps.isNotEmpty) { | ||||||
|       for (var app in apps.values) { |       await saveApps(modifiedApps, attemptToCorrectInstallStatus: false); | ||||||
|         var moddedApp = |       var removedAppIds = modifiedApps | ||||||
|             getCorrectedInstallStatusAppIfPossible(app.app, app.installedInfo); |           .where((a) => a.installedVersion == null) | ||||||
|         if (moddedApp != null) { |           .map((e) => e.id) | ||||||
|           modifiedApps.add(moddedApp); |           .toList(); | ||||||
|         } |       // After reconciliation, delete externally uninstalled Apps if needed | ||||||
|       } |       if (removedAppIds.isNotEmpty) { | ||||||
|       if (modifiedApps.isNotEmpty) { |         var settingsProvider = SettingsProvider(); | ||||||
|         await saveApps(modifiedApps, attemptToCorrectInstallStatus: false); |         await settingsProvider.initializeSettings(); | ||||||
|         var removedAppIds = modifiedApps |         if (settingsProvider.removeOnExternalUninstall) { | ||||||
|             .where((a) => a.installedVersion == null) |           await removeApps(removedAppIds); | ||||||
|             .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); |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -834,12 +891,12 @@ class AppsProvider with ChangeNotifier { | |||||||
|   Future<void> saveApps(List<App> apps, |   Future<void> saveApps(List<App> apps, | ||||||
|       {bool attemptToCorrectInstallStatus = true, |       {bool attemptToCorrectInstallStatus = true, | ||||||
|       bool onlyIfExists = true}) async { |       bool onlyIfExists = true}) async { | ||||||
|     attemptToCorrectInstallStatus = |     attemptToCorrectInstallStatus = attemptToCorrectInstallStatus; | ||||||
|         attemptToCorrectInstallStatus && (await doesInstalledAppsPluginWork()); |  | ||||||
|     for (var a in apps) { |     for (var a in apps) { | ||||||
|       var app = a.deepCopy(); |       var app = a.deepCopy(); | ||||||
|       AppInfo? info = await getInstalledInfo(app.id); |       PackageInfo? info = await getInstalledInfo(app.id); | ||||||
|       app.name = info?.name ?? app.name; |       var icon = await info?.applicationInfo?.getAppIcon(); | ||||||
|  |       app.name = await (info?.applicationInfo?.getAppLabel()) ?? app.name; | ||||||
|       if (attemptToCorrectInstallStatus) { |       if (attemptToCorrectInstallStatus) { | ||||||
|         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; |         app = getCorrectedInstallStatusAppIfPossible(app, info) ?? app; | ||||||
|       } |       } | ||||||
| @@ -848,9 +905,10 @@ class AppsProvider with ChangeNotifier { | |||||||
|             .writeAsStringSync(jsonEncode(app.toJson())); |             .writeAsStringSync(jsonEncode(app.toJson())); | ||||||
|       } |       } | ||||||
|       try { |       try { | ||||||
|         this.apps.update( |         this.apps.update(app.id, | ||||||
|             app.id, (value) => AppInMemory(app, value.downloadProgress, info), |             (value) => AppInMemory(app, value.downloadProgress, info, icon), | ||||||
|             ifAbsent: onlyIfExists ? null : () => AppInMemory(app, null, info)); |             ifAbsent: | ||||||
|  |                 onlyIfExists ? null : () => AppInMemory(app, null, info, icon)); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         if (e is! ArgumentError || e.name != 'key') { |         if (e is! ArgumentError || e.name != 'key') { | ||||||
|           rethrow; |           rethrow; | ||||||
| @@ -965,6 +1023,22 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return newApp.latestVersion != currentApp.latestVersion ? newApp : null; |     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( |   Future<List<App>> checkUpdates( | ||||||
|       {DateTime? ignoreAppsCheckedAfter, |       {DateTime? ignoreAppsCheckedAfter, | ||||||
|       bool throwErrorsForRetry = false}) async { |       bool throwErrorsForRetry = false}) async { | ||||||
| @@ -973,17 +1047,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|     if (!gettingUpdates) { |     if (!gettingUpdates) { | ||||||
|       gettingUpdates = true; |       gettingUpdates = true; | ||||||
|       try { |       try { | ||||||
|         List<String> appIds = apps.values |         List<String> appIds = getAppsSortedByUpdateCheckTime( | ||||||
|             .where((app) => |             ignoreAppsCheckedAfter: ignoreAppsCheckedAfter); | ||||||
|                 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))); |  | ||||||
|         for (int i = 0; i < appIds.length; i++) { |         for (int i = 0; i < appIds.length; i++) { | ||||||
|           App? newApp; |           App? newApp; | ||||||
|           try { |           try { | ||||||
| @@ -1200,3 +1265,221 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Background updater function | ||||||
|  | /// | ||||||
|  | /// @param List<String>? toCheck: The appIds to check for updates (default to all apps sorted by last update check time) | ||||||
|  | /// | ||||||
|  | /// @param List<String>? toInstall: The appIds to attempt to update (defaults to an empty array) | ||||||
|  | /// | ||||||
|  | /// @param int? attemptCount: The number of times the function has failed up to this point (defaults to 0) | ||||||
|  | /// | ||||||
|  | /// When toCheck is empty, the function is in "install mode" (else it is in "update mode"). | ||||||
|  | /// In update mode, all apps in toCheck are checked for updates. | ||||||
|  | /// If an update is available, the appId is either added to toInstall (if a background update is possible) or the user is notified. | ||||||
|  | /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. | ||||||
|  | /// | ||||||
|  | /// Once all update checks are complete, the function is called again in install mode. | ||||||
|  | /// In this mode, all apps in toInstall are downloaded and installed in the background (install result is unknown). | ||||||
|  | /// If there is an error, the function tries to continue after a few minutes (duration depends on the error), up to a maximum of 5 tries. | ||||||
|  | /// | ||||||
|  | /// In either mode, if the function fails after the maximum number of tries, the user is notified. | ||||||
|  | @pragma('vm:entry-point') | ||||||
|  | Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | ||||||
|  |   WidgetsFlutterBinding.ensureInitialized(); | ||||||
|  |   await EasyLocalization.ensureInitialized(); | ||||||
|  |   await AndroidAlarmManager.initialize(); | ||||||
|  |   await loadTranslations(); | ||||||
|  |  | ||||||
|  |   LogsProvider logs = LogsProvider(); | ||||||
|  |   NotificationsProvider notificationsProvider = NotificationsProvider(); | ||||||
|  |   AppsProvider appsProvider = AppsProvider(isBg: true); | ||||||
|  |   await appsProvider.loadApps(); | ||||||
|  |   var settingsProvider = SettingsProvider(); | ||||||
|  |   await settingsProvider.initializeSettings(); | ||||||
|  |  | ||||||
|  |   int maxAttempts = 4; | ||||||
|  |  | ||||||
|  |   params ??= {}; | ||||||
|  |   if (params['toCheck'] == null) { | ||||||
|  |     settingsProvider.lastBGCheckTime = DateTime.now(); | ||||||
|  |   } | ||||||
|  |   List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[ | ||||||
|  |     ...(params['toCheck'] | ||||||
|  |             ?.map((entry) => MapEntry<String, int>( | ||||||
|  |                 entry['key'] as String, entry['value'] as int)) | ||||||
|  |             .toList() ?? | ||||||
|  |         appsProvider | ||||||
|  |             .getAppsSortedByUpdateCheckTime() | ||||||
|  |             .map((e) => MapEntry(e, 0))) | ||||||
|  |   ]; | ||||||
|  |   List<MapEntry<String, int>> toInstall = <MapEntry<String, int>>[ | ||||||
|  |     ...(params['toInstall'] | ||||||
|  |             ?.map((entry) => MapEntry<String, int>( | ||||||
|  |                 entry['key'] as String, entry['value'] as int)) | ||||||
|  |             .toList() ?? | ||||||
|  |         (<List<MapEntry<String, int>>>[])) | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   bool installMode = toCheck.isEmpty && | ||||||
|  |       toInstall.isNotEmpty; // Task is either in update mode or install mode | ||||||
|  |  | ||||||
|  |   logs.add( | ||||||
|  |       'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).'); | ||||||
|  |  | ||||||
|  |   if (!installMode) { | ||||||
|  |     // If in update mode... | ||||||
|  |     var didCompleteChecking = false; | ||||||
|  |     CheckingUpdatesNotification? notif; | ||||||
|  |     var networkRestricted = false; | ||||||
|  |     if (settingsProvider.bgUpdatesOnWiFiOnly) { | ||||||
|  |       var netResult = await (Connectivity().checkConnectivity()); | ||||||
|  |       networkRestricted = (netResult != ConnectivityResult.wifi) && | ||||||
|  |           (netResult != ConnectivityResult.ethernet); | ||||||
|  |     } | ||||||
|  |     // Loop through all updates and check each | ||||||
|  |     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, settingsProvider))) { | ||||||
|  |                 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, settingsProvider, | ||||||
|  |             notificationsProvider: notificationsProvider); | ||||||
|  |         await Future.delayed(const Duration( | ||||||
|  |             seconds: | ||||||
|  |                 5)); // Just in case task ending causes install fail (not clear) | ||||||
|  |         if (i == (toCheck.length - 1)) { | ||||||
|  |           didCompleteInstalling = true; | ||||||
|  |         } | ||||||
|  |       } catch (e) { | ||||||
|  |         // If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly | ||||||
|  |         logs.add( | ||||||
|  |             'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.'); | ||||||
|  |         if (retryCount < maxAttempts) { | ||||||
|  |           var remainingSeconds = retryCount; | ||||||
|  |           logs.add( | ||||||
|  |               'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); | ||||||
|  |           var remainingToInstall = moveStrToEndMapEntryWithCount( | ||||||
|  |               toInstall.sublist(i), MapEntry(appId, retryCount + 1)); | ||||||
|  |           AndroidAlarmManager.oneShot( | ||||||
|  |               Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, | ||||||
|  |               params: { | ||||||
|  |                 'toCheck': toCheck | ||||||
|  |                     .map((entry) => {'key': entry.key, 'value': entry.value}) | ||||||
|  |                     .toList(), | ||||||
|  |                 'toInstall': remainingToInstall | ||||||
|  |                     .map((entry) => {'key': entry.key, 'value': entry.value}) | ||||||
|  |                     .toList(), | ||||||
|  |               }); | ||||||
|  |           break; | ||||||
|  |         } else { | ||||||
|  |           // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) | ||||||
|  |           toInstall.removeAt(i); | ||||||
|  |           i--; | ||||||
|  |           notificationsProvider | ||||||
|  |               .notify(ErrorCheckingUpdatesNotification(e.toString())); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (didCompleteInstalling) { | ||||||
|  |         logs.add('BG install task $taskId: Done.'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -22,9 +22,9 @@ class ObtainiumNotification { | |||||||
| } | } | ||||||
|  |  | ||||||
| class UpdateNotification extends ObtainiumNotification { | class UpdateNotification extends ObtainiumNotification { | ||||||
|   UpdateNotification(List<App> updates) |   UpdateNotification(List<App> updates, {int? id}) | ||||||
|       : super( |       : super( | ||||||
|             2, |             id ?? 2, | ||||||
|             tr('updatesAvailable'), |             tr('updatesAvailable'), | ||||||
|             '', |             '', | ||||||
|             'UPDATES_AVAILABLE', |             'UPDATES_AVAILABLE', | ||||||
| @@ -41,8 +41,8 @@ class UpdateNotification extends ObtainiumNotification { | |||||||
| } | } | ||||||
|  |  | ||||||
| class SilentUpdateNotification extends ObtainiumNotification { | class SilentUpdateNotification extends ObtainiumNotification { | ||||||
|   SilentUpdateNotification(List<App> updates) |   SilentUpdateNotification(List<App> updates, {int? id}) | ||||||
|       : super(3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'), |       : super(id ?? 3, tr('appsUpdated'), '', 'APPS_UPDATED', tr('appsUpdated'), | ||||||
|             tr('appsUpdatedNotifDescription'), Importance.defaultImportance) { |             tr('appsUpdatedNotifDescription'), Importance.defaultImportance) { | ||||||
|     message = updates.length == 1 |     message = updates.length == 1 | ||||||
|         ? tr('xWasUpdatedToY', |         ? tr('xWasUpdatedToY', | ||||||
| @@ -52,10 +52,28 @@ class SilentUpdateNotification extends ObtainiumNotification { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class ErrorCheckingUpdatesNotification extends ObtainiumNotification { | class SilentUpdateAttemptNotification extends ObtainiumNotification { | ||||||
|   ErrorCheckingUpdatesNotification(String error) |   SilentUpdateAttemptNotification(List<App> updates, {int? id}) | ||||||
|       : super( |       : 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'), |             tr('errorCheckingUpdates'), | ||||||
|             error, |             error, | ||||||
|             'BG_UPDATE_CHECK_ERROR', |             'BG_UPDATE_CHECK_ERROR', | ||||||
| @@ -99,14 +117,17 @@ final completeInstallationNotification = ObtainiumNotification( | |||||||
|     tr('completeAppInstallationNotifDescription'), |     tr('completeAppInstallationNotifDescription'), | ||||||
|     Importance.max); |     Importance.max); | ||||||
|  |  | ||||||
| final checkingUpdatesNotification = ObtainiumNotification( | class CheckingUpdatesNotification extends ObtainiumNotification { | ||||||
|     4, |   CheckingUpdatesNotification(String appName) | ||||||
|     tr('checkingForUpdates'), |       : super( | ||||||
|     '', |             4, | ||||||
|     'BG_UPDATE_CHECK', |             tr('checkingForUpdates'), | ||||||
|     tr('checkingForUpdates'), |             appName, | ||||||
|     tr('checkingForUpdatesNotifDescription'), |             'BG_UPDATE_CHECK', | ||||||
|     Importance.min); |             tr('checkingForUpdates'), | ||||||
|  |             tr('checkingForUpdatesNotifDescription'), | ||||||
|  |             Importance.min); | ||||||
|  | } | ||||||
|  |  | ||||||
| class NotificationsProvider { | class NotificationsProvider { | ||||||
|   FlutterLocalNotificationsPlugin notifications = |   FlutterLocalNotificationsPlugin notifications = | ||||||
|   | |||||||
| @@ -309,4 +309,43 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     prefs?.setBool('reversePageTransitions', show); |     prefs?.setBool('reversePageTransitions', show); | ||||||
|     notifyListeners(); |     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(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,11 +9,13 @@ import 'package:html/dom.dart'; | |||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:obtainium/app_sources/apkmirror.dart'; | import 'package:obtainium/app_sources/apkmirror.dart'; | ||||||
| import 'package:obtainium/app_sources/apkpure.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/codeberg.dart'; | ||||||
| import 'package:obtainium/app_sources/fdroid.dart'; | import 'package:obtainium/app_sources/fdroid.dart'; | ||||||
| import 'package:obtainium/app_sources/fdroidrepo.dart'; | import 'package:obtainium/app_sources/fdroidrepo.dart'; | ||||||
| import 'package:obtainium/app_sources/github.dart'; | import 'package:obtainium/app_sources/github.dart'; | ||||||
| import 'package:obtainium/app_sources/gitlab.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/izzyondroid.dart'; | ||||||
| import 'package:obtainium/app_sources/html.dart'; | import 'package:obtainium/app_sources/html.dart'; | ||||||
| import 'package:obtainium/app_sources/jenkins.dart'; | import 'package:obtainium/app_sources/jenkins.dart'; | ||||||
| @@ -324,21 +326,29 @@ abstract class AppSource { | |||||||
|   bool enforceTrackOnly = false; |   bool enforceTrackOnly = false; | ||||||
|   bool changeLogIfAnyIsMarkDown = true; |   bool changeLogIfAnyIsMarkDown = true; | ||||||
|   bool appIdInferIsOptional = false; |   bool appIdInferIsOptional = false; | ||||||
|  |   bool allowSubDomains = false; | ||||||
|  |  | ||||||
|   AppSource() { |   AppSource() { | ||||||
|     name = runtimeType.toString(); |     name = runtimeType.toString(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   overrideVersionDetectionFormDefault(String vd, bool disableStandard) { |   overrideVersionDetectionFormDefault(String vd, | ||||||
|  |       {bool disableStandard = false, bool disableRelDate = false}) { | ||||||
|     additionalAppSpecificSourceAgnosticSettingFormItems = |     additionalAppSpecificSourceAgnosticSettingFormItems = | ||||||
|         additionalAppSpecificSourceAgnosticSettingFormItems.map((e) { |         additionalAppSpecificSourceAgnosticSettingFormItems.map((e) { | ||||||
|       return e.map((e2) { |       return e.map((e2) { | ||||||
|         if (e2.key == 'versionDetection') { |         if (e2.key == 'versionDetection') { | ||||||
|           var item = e2 as GeneratedFormDropdown; |           var item = e2 as GeneratedFormDropdown; | ||||||
|           item.defaultValue = vd; |           item.defaultValue = vd; | ||||||
|  |           item.disabledOptKeys = []; | ||||||
|           if (disableStandard) { |           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; |         return e2; | ||||||
|       }).toList(); |       }).toList(); | ||||||
| @@ -355,10 +365,14 @@ abstract class AppSource { | |||||||
|  |  | ||||||
|   Map<String, String>? get requestHeaders => null; |   Map<String, String>? get requestHeaders => null; | ||||||
|  |  | ||||||
|   Future<Response> sourceRequest(String url) async { |   Future<Response> sourceRequest(String url, | ||||||
|     if (requestHeaders != null) { |       {bool followRedirects = true}) async { | ||||||
|  |     if (requestHeaders != null || followRedirects == false) { | ||||||
|       var req = Request('GET', Uri.parse(url)); |       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)); |       return Response.fromStream(await Client().send(req)); | ||||||
|     } else { |     } else { | ||||||
|       return get(Uri.parse(url)); |       return get(Uri.parse(url)); | ||||||
| @@ -413,7 +427,11 @@ abstract class AppSource { | |||||||
|       GeneratedFormSwitch('autoApkFilterByArch', |       GeneratedFormSwitch('autoApkFilterByArch', | ||||||
|           label: tr('autoApkFilterByArch'), defaultValue: true) |           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 |   // Previous 2 variables combined into one at runtime for convenient usage | ||||||
| @@ -506,8 +524,10 @@ class SourceProvider { | |||||||
|         Jenkins(), |         Jenkins(), | ||||||
|         SourceForge(), |         SourceForge(), | ||||||
|         SourceHut(), |         SourceHut(), | ||||||
|  |         Aptoide(), | ||||||
|         APKMirror(), |         APKMirror(), | ||||||
|         APKPure(), |         APKPure(), | ||||||
|  |         HuaweiAppGallery(), | ||||||
|         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) |         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) | ||||||
|         Mullvad(), |         Mullvad(), | ||||||
|         Signal(), |         Signal(), | ||||||
| @@ -537,7 +557,9 @@ class SourceProvider { | |||||||
|     } |     } | ||||||
|     AppSource? source; |     AppSource? source; | ||||||
|     for (var s in sources.where((element) => element.host != null)) { |     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; |         source = s; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|   | |||||||
							
								
								
									
										246
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										246
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -22,26 +22,34 @@ packages: | |||||||
|     description: |     description: | ||||||
|       path: "." |       path: "." | ||||||
|       ref: main |       ref: main | ||||||
|       resolved-ref: "2edf5dbbfeeb33257d526861f2a992aee5d97bb4" |       resolved-ref: ba2aa7a11edc2649d1d80c25ed9291521262f714 | ||||||
|       url: "https://github.com/ImranR98/android_package_installer" |       url: "https://github.com/ImranR98/android_package_installer" | ||||||
|     source: git |     source: git | ||||||
|     version: "0.0.1" |     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: |   animations: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: animations |       name: animations | ||||||
|       sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 |       sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.7" |     version: "2.0.8" | ||||||
|   archive: |   archive: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: archive |       name: archive | ||||||
|       sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" |       sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.3.7" |     version: "3.3.8" | ||||||
|   args: |   args: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -102,10 +110,26 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: collection |       name: collection | ||||||
|       sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" |       sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     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: |   convert: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -118,10 +142,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: cross_file |       name: cross_file | ||||||
|       sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" |       sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.3.3+4" |     version: "0.3.3+5" | ||||||
|   crypto: |   crypto: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -142,10 +166,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: cupertino_icons |       name: cupertino_icons | ||||||
|       sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be |       sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.5" |     version: "1.0.6" | ||||||
|   dbus: |   dbus: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -182,10 +206,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: easy_localization |       name: easy_localization | ||||||
|       sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645" |       sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.2" |     version: "3.0.3" | ||||||
|   easy_logger: |   easy_logger: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -222,10 +246,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: file_picker |       name: file_picker | ||||||
|       sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" |       sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.3.3" |     version: "5.5.0" | ||||||
|   flutter: |   flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -259,18 +283,18 @@ packages: | |||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
|       name: flutter_lints |       name: flutter_lints | ||||||
|       sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" |       sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.2" |     version: "2.0.3" | ||||||
|   flutter_local_notifications: |   flutter_local_notifications: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_local_notifications |       name: flutter_local_notifications | ||||||
|       sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975" |       sha256: "3002092e5b8ce2f86c3361422e52e6db6776c23ee21e0b2f71b892bf4259ef04" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "15.1.0+1" |     version: "15.1.1" | ||||||
|   flutter_local_notifications_linux: |   flutter_local_notifications_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -296,18 +320,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_markdown |       name: flutter_markdown | ||||||
|       sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" |       sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.6.17+1" |     version: "0.6.17+2" | ||||||
|   flutter_plugin_android_lifecycle: |   flutter_plugin_android_lifecycle: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: flutter_plugin_android_lifecycle |       name: flutter_plugin_android_lifecycle | ||||||
|       sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" |       sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.15" |     version: "2.0.16" | ||||||
|   flutter_test: |   flutter_test: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -366,22 +390,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.0.17" |     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: |   intl: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: intl |       name: intl | ||||||
|       sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 |       sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.18.0" |     version: "0.18.1" | ||||||
|   js: |   js: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -418,18 +434,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: matcher |       name: matcher | ||||||
|       sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" |       sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.12.15" |     version: "0.12.16" | ||||||
|   material_color_utilities: |   material_color_utilities: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: material_color_utilities |       name: material_color_utilities | ||||||
|       sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 |       sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.0" |     version: "0.5.0" | ||||||
|   meta: |   meta: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -454,22 +470,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "1.0.0" | ||||||
|   package_archive_info: |   nm: | ||||||
|     dependency: "direct main" |  | ||||||
|     description: |  | ||||||
|       name: package_archive_info |  | ||||||
|       sha256: "8f671a29b79d15f192e5e2b0dab9d0bad66b9ee93fb58d4e0afdb62f91a259be" |  | ||||||
|       url: "https://pub.dev" |  | ||||||
|     source: hosted |  | ||||||
|     version: "0.1.0" |  | ||||||
|   package_info: |  | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: package_info |       name: nm | ||||||
|       sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" |       sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.2" |     version: "0.5.0" | ||||||
|   path: |   path: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -482,50 +490,50 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: path_provider |       name: path_provider | ||||||
|       sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" |       sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.1.1" | ||||||
|   path_provider_android: |   path_provider_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_android |       name: path_provider_android | ||||||
|       sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" |       sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.2.0" | ||||||
|   path_provider_foundation: |   path_provider_foundation: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_foundation |       name: path_provider_foundation | ||||||
|       sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" |       sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.1" | ||||||
|   path_provider_linux: |   path_provider_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_linux |       name: path_provider_linux | ||||||
|       sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 |       sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.1" | ||||||
|   path_provider_platform_interface: |   path_provider_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_platform_interface |       name: path_provider_platform_interface | ||||||
|       sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 |       sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.1.1" | ||||||
|   path_provider_windows: |   path_provider_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: path_provider_windows |       name: path_provider_windows | ||||||
|       sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da |       sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.1" | ||||||
|   permission_handler: |   permission_handler: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -538,10 +546,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_android |       name: permission_handler_android | ||||||
|       sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" |       sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.3.3" |     version: "10.3.4" | ||||||
|   permission_handler_apple: |   permission_handler_apple: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -578,18 +586,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: platform |       name: platform | ||||||
|       sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" |       sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.0" |     version: "3.1.2" | ||||||
|   plugin_platform_interface: |   plugin_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: plugin_platform_interface |       name: plugin_platform_interface | ||||||
|       sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" |       sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.5" |     version: "2.1.6" | ||||||
|   pointycastle: |   pointycastle: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -626,58 +634,58 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences |       name: shared_preferences | ||||||
|       sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" |       sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.1" | ||||||
|   shared_preferences_android: |   shared_preferences_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_android |       name: shared_preferences_android | ||||||
|       sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 |       sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.1" | ||||||
|   shared_preferences_foundation: |   shared_preferences_foundation: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_foundation |       name: shared_preferences_foundation | ||||||
|       sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef |       sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.3" |     version: "2.3.4" | ||||||
|   shared_preferences_linux: |   shared_preferences_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_linux |       name: shared_preferences_linux | ||||||
|       sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" |       sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.1" | ||||||
|   shared_preferences_platform_interface: |   shared_preferences_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_platform_interface |       name: shared_preferences_platform_interface | ||||||
|       sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" |       sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.1" | ||||||
|   shared_preferences_web: |   shared_preferences_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_web |       name: shared_preferences_web | ||||||
|       sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" |       sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.1" | ||||||
|   shared_preferences_windows: |   shared_preferences_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: shared_preferences_windows |       name: shared_preferences_windows | ||||||
|       sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d |       sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.0" |     version: "2.3.1" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -687,10 +695,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: source_span |       name: source_span | ||||||
|       sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 |       sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.9.1" |     version: "1.10.0" | ||||||
|   sqflite: |   sqflite: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -751,10 +759,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: test_api |       name: test_api | ||||||
|       sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb |       sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.1" |     version: "0.6.0" | ||||||
|   timezone: |   timezone: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -775,66 +783,66 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: url_launcher |       name: url_launcher | ||||||
|       sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" |       sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.1.12" |     version: "6.1.14" | ||||||
|   url_launcher_android: |   url_launcher_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_android |       name: url_launcher_android | ||||||
|       sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" |       sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.38" |     version: "6.1.0" | ||||||
|   url_launcher_ios: |   url_launcher_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_ios |       name: url_launcher_ios | ||||||
|       sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" |       sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.1.4" |     version: "6.1.5" | ||||||
|   url_launcher_linux: |   url_launcher_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_linux |       name: url_launcher_linux | ||||||
|       sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" |       sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.5" |     version: "3.0.6" | ||||||
|   url_launcher_macos: |   url_launcher_macos: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_macos |       name: url_launcher_macos | ||||||
|       sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" |       sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.6" |     version: "3.0.7" | ||||||
|   url_launcher_platform_interface: |   url_launcher_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_platform_interface |       name: url_launcher_platform_interface | ||||||
|       sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea |       sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.3" |     version: "2.1.5" | ||||||
|   url_launcher_web: |   url_launcher_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_web |       name: url_launcher_web | ||||||
|       sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 |       sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.18" |     version: "2.0.20" | ||||||
|   url_launcher_windows: |   url_launcher_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_windows |       name: url_launcher_windows | ||||||
|       sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" |       sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.7" |     version: "3.0.8" | ||||||
|   uuid: |   uuid: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -851,46 +859,54 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.4" |     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: |   webview_flutter: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter |       name: webview_flutter | ||||||
|       sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00" |       sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.2.2" |     version: "4.2.4" | ||||||
|   webview_flutter_android: |   webview_flutter_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_android |       name: webview_flutter_android | ||||||
|       sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6 |       sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.9.3" |     version: "3.9.5" | ||||||
|   webview_flutter_platform_interface: |   webview_flutter_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_platform_interface |       name: webview_flutter_platform_interface | ||||||
|       sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30" |       sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.0" |     version: "2.5.1" | ||||||
|   webview_flutter_wkwebview: |   webview_flutter_wkwebview: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_wkwebview |       name: webview_flutter_wkwebview | ||||||
|       sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972 |       sha256: d2f7241849582da80b79acb03bb936422412ce5c0c79fb5f6a1de5421a5aecc4 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.7.3" |     version: "3.7.4" | ||||||
|   win32: |   win32: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: win32 |       name: win32 | ||||||
|       sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 |       sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "5.0.6" |     version: "5.0.7" | ||||||
|   win32_registry: |   win32_registry: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -903,10 +919,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: xdg_directories |       name: xdg_directories | ||||||
|       sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 |       sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.2" |     version: "1.0.3" | ||||||
|   xml: |   xml: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -924,5 +940,5 @@ packages: | |||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.1.2" |     version: "3.1.2" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=3.0.0 <4.0.0" |   dart: ">=3.1.0 <4.0.0" | ||||||
|   flutter: ">=3.10.0" |   flutter: ">=3.13.0" | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| name: obtainium | 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 | # The following line prevents the package from being accidentally published to | ||||||
| # pub.dev using `flutter pub publish`. This is preferred for private packages. | # pub.dev using `flutter pub publish`. This is preferred for private packages. | ||||||
| @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 0.13.24+188 # When changing this, update the tag in main() accordingly | version: 0.14.10+202 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=2.18.2 <3.0.0' | ||||||
| @@ -55,9 +55,8 @@ dependencies: | |||||||
|     git: |     git: | ||||||
|       url: https://github.com/ImranR98/android_package_installer |       url: https://github.com/ImranR98/android_package_installer | ||||||
|       ref: main |       ref: main | ||||||
|  |   android_package_manager: ^0.6.0 | ||||||
|   share_plus: ^7.0.0 |   share_plus: ^7.0.0 | ||||||
|   installed_apps: ^1.3.1 |  | ||||||
|   package_archive_info: ^0.1.0 |  | ||||||
|   android_alarm_manager_plus: ^3.0.0 |   android_alarm_manager_plus: ^3.0.0 | ||||||
|   sqflite: ^2.2.0+3 |   sqflite: ^2.2.0+3 | ||||||
|   easy_localization: ^3.0.1 |   easy_localization: ^3.0.1 | ||||||
| @@ -65,6 +64,7 @@ dependencies: | |||||||
|   flutter_markdown: ^0.6.14 |   flutter_markdown: ^0.6.14 | ||||||
|   flutter_archive: ^5.0.0 |   flutter_archive: ^5.0.0 | ||||||
|   hsluv: ^1.1.3 |   hsluv: ^1.1.3 | ||||||
|  |   connectivity_plus: ^4.0.2 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user