mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 13:33:28 +01:00 
			
		
		
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			v0.14.5-be
			...
			v0.14.11-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d7348b4973 | ||
|  | 09421230f2 | ||
|  | 4596e32258 | ||
|  | 4dc007a4f6 | ||
|  | c53a156969 | ||
|  | 94bd0774fb | ||
|  | b178b1d780 | ||
|  | cbc840378c | ||
|  | aa7989c16d | ||
|  | 85f9336804 | ||
|  | d66be3ecda | ||
|  | c08e05bd6c | ||
|  | e08ab89fd4 | ||
|  | 8ba0a0a776 | ||
|  | 73ed0cea88 | ||
|  | 58a378d212 | ||
|  | 553307ba70 | ||
|  | 78f73a9049 | ||
|  | abc69e7a0e | ||
|  | 503914dbce | ||
|  | 69680f8680 | ||
|  | 1a616aacb7 | ||
|  | 52d19f267e | ||
|  | 75a8dc9ee4 | ||
|  | 8c63920ef6 | ||
|  | dda5619cdc | ||
|  | 9cc25fd931 | ||
|  | f5a9eb4295 | ||
|  | ebe256e482 | ||
|  | e215585a64 | ||
|  | 8b01fc03ec | ||
|  | 9af2c8370d | ||
|  | 01f9003b8d | ||
|  | 73a3c7eb71 | ||
|  | 370ec1432e | 
							
								
								
									
										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. --> | ||||||
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,30 +2,36 @@ | |||||||
|  |  | ||||||
| Get Android App Updates Directly From the Source. | Get Android App Updates Directly From the Source. | ||||||
|  |  | ||||||
| Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available. | Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||||
|  |  | ||||||
| Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | ||||||
|  |  | ||||||
| Currently supported App sources: | Currently supported App sources: | ||||||
| - [GitHub](https://github.com/) | - Open Source - General: | ||||||
| - [GitLab](https://gitlab.com/) |   - [GitHub](https://github.com/) | ||||||
| - [Codeberg](https://codeberg.org/) |   - [GitLab](https://gitlab.com/) | ||||||
| - [F-Droid](https://f-droid.org/) |   - [Codeberg](https://codeberg.org/) | ||||||
| - [IzzyOnDroid](https://android.izzysoft.de/) |   - [F-Droid](https://f-droid.org/) | ||||||
| - [Mullvad](https://mullvad.net/en/) |   - Third Party F-Droid Repos | ||||||
| - [Signal](https://signal.org/) |   - [IzzyOnDroid](https://android.izzysoft.de/) | ||||||
| - [SourceForge](https://sourceforge.net/) |   - [SourceForge](https://sourceforge.net/) | ||||||
| - [SourceHut](https://git.sr.ht/) |   - [SourceHut](https://git.sr.ht/) | ||||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | - Other - General: | ||||||
| - [APKPure](https://apkpure.com/) |   - [APKPure](https://apkpure.com/) | ||||||
| - [Huawei AppGallery](https://appgallery.huawei.com/) |   - [Aptoide](https://aptoide.com/) | ||||||
| - Third Party F-Droid Repos |   - [Uptodown](https://uptodown.com/) | ||||||
| - Jenkins Jobs |   - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||||
| - [Steam](https://store.steampowered.com/mobile) |   - [Huawei AppGallery](https://appgallery.huawei.com/) | ||||||
| - [Telegram App](https://telegram.org) |   - Jenkins Jobs | ||||||
| - [Neutron Code](https://neutroncode.com) | - Open Source - App-Specific: | ||||||
| - "HTML" (Fallback) |   - [Mullvad](https://mullvad.net/en/) | ||||||
|   - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) |   - [Signal](https://signal.org/) | ||||||
|  |   - [VLC](https://videolan.org/) | ||||||
|  | - Other - App-Specific: | ||||||
|  |   - [Telegram App](https://telegram.org) | ||||||
|  |   - [Steam Mobile Apps](https://store.steampowered.com/mobile) | ||||||
|  |   - [Neutron Code](https://neutroncode.com) | ||||||
|  | - "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Ok", |     "ok": "Ok", | ||||||
|     "and": "e", |     "and": "e", | ||||||
|     "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", |     "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", |     "includePrereleases": "Incluir pré-lançamentos", | ||||||
|     "fallbackToOlderReleases": "Retornar para versões anteriores", |     "fallbackToOlderReleases": "Retornar para versões anteriores", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", |     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", |     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", | ||||||
|     "search": "Procurar", |     "search": "Procurar", | ||||||
|     "additionalOptsFor": "Opções Adicionais para {}", |     "additionalOptsFor": "Opções Adicionais para {}", | ||||||
|     "supportedSourcesBelow": "Fontes Compatíveis:", |     "supportedSources": "Fontes Compatíveis", | ||||||
|     "trackOnlyInBrackets": "(Apenas Seguir)", |     "trackOnlyInBrackets": "(Apenas Seguir)", | ||||||
|     "searchableInBrackets": "(Pesquisável)", |     "searchableInBrackets": "(Pesquisável)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -252,6 +250,10 @@ | |||||||
|     "intermediateLinkNotFound": "Link intermediário não encontrado", |     "intermediateLinkNotFound": "Link intermediário não encontrado", | ||||||
|     "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", |     "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", |     "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remover App?", |         "one": "Remover App?", | ||||||
|         "other": "Remover Apps?" |         "other": "Remover Apps?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|    "ok": "Dobro", |    "ok": "Dobro", | ||||||
|    "and": "i", |    "and": "i", | ||||||
|    "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", |  | ||||||
|    "githubPATFormat": "korisničko_ime:token", |  | ||||||
|    "includePrereleases": "Uključi preliminarna izdanja", |    "includePrereleases": "Uključi preliminarna izdanja", | ||||||
|    "fallbackToOlderReleases": "Povratak na starija izdanja", |    "fallbackToOlderReleases": "Povratak na starija izdanja", | ||||||
|    "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", |    "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|    "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", |    "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", | ||||||
|    "search": "Pretraživanje", |    "search": "Pretraživanje", | ||||||
|    "additionalOptsFor": "Dodatne opcije za {}", |    "additionalOptsFor": "Dodatne opcije za {}", | ||||||
|    "supportedSourcesBelow": "Podržani izvori:", |    "supportedSources": "Podržani izvori", | ||||||
|    "trackOnlyInBrackets": "(Samo za praćenje)", |    "trackOnlyInBrackets": "(Samo za praćenje)", | ||||||
|    "searchableInBrackets": "(Može se pretraživati)", |    "searchableInBrackets": "(Može se pretraživati)", | ||||||
|    "appsString": "Aplikacije", |    "appsString": "Aplikacije", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|    "verifyLatestTag": "Verify the 'latest' tag", |    "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |    "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |    "matchGroupToUse": "Match Group to Use", | ||||||
|  |    "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|    "removeAppQuestion": { |    "removeAppQuestion": { | ||||||
|       "one": "Želite li ukloniti aplikaciju?", |       "one": "Želite li ukloniti aplikaciju?", | ||||||
|       "other": "Želite li ukloniti aplikacije?" |       "other": "Želite li ukloniti aplikacije?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "und", |     "and": "und", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "Benutzername:Token", |  | ||||||
|     "includePrereleases": "Vorabversionen einbeziehen", |     "includePrereleases": "Vorabversionen einbeziehen", | ||||||
|     "fallbackToOlderReleases": "Fallback auf ältere Versionen", |     "fallbackToOlderReleases": "Fallback auf ältere Versionen", | ||||||
|     "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", |     "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)", |     "searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)", | ||||||
|     "search": "Suchen", |     "search": "Suchen", | ||||||
|     "additionalOptsFor": "Zusatzoptionen für {}", |     "additionalOptsFor": "Zusatzoptionen für {}", | ||||||
|     "supportedSourcesBelow": "Unterstützte Quellen:", |     "supportedSources": "Unterstützte Quellen", | ||||||
|     "trackOnlyInBrackets": "(Nur Nachverfolgen)", |     "trackOnlyInBrackets": "(Nur Nachverfolgen)", | ||||||
|     "searchableInBrackets": "(Durchsuchbar)", |     "searchableInBrackets": "(Durchsuchbar)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Überprüfe das 'latest' Tag", |     "verifyLatestTag": "Überprüfe das 'latest' Tag", | ||||||
|     "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", |     "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", |     "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "App entfernen?", |         "one": "App entfernen?", | ||||||
|         "other": "Apps entfernen?" |         "other": "Apps entfernen?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "and", |     "and": "and", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "Include prereleases", |     "includePrereleases": "Include prereleases", | ||||||
|     "fallbackToOlderReleases": "Fallback to older releases", |     "fallbackToOlderReleases": "Fallback to older releases", | ||||||
|     "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", |     "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Search (Some Sources Only)", |     "searchSomeSourcesLabel": "Search (Some Sources Only)", | ||||||
|     "search": "Search", |     "search": "Search", | ||||||
|     "additionalOptsFor": "Additional Options for {}", |     "additionalOptsFor": "Additional Options for {}", | ||||||
|     "supportedSourcesBelow": "Supported Sources:", |     "supportedSources": "Supported Sources", | ||||||
|     "trackOnlyInBrackets": "(Track-Only)", |     "trackOnlyInBrackets": "(Track-Only)", | ||||||
|     "searchableInBrackets": "(Searchable)", |     "searchableInBrackets": "(Searchable)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -252,6 +250,10 @@ | |||||||
|     "intermediateLinkNotFound": "Intermediate link not found", |     "intermediateLinkNotFound": "Intermediate link not found", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remove App?", |         "one": "Remove App?", | ||||||
|         "other": "Remove Apps?" |         "other": "Remove Apps?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Correcto", |     "ok": "Correcto", | ||||||
|     "and": "y", |     "and": "y", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "nombre_de_usuario:token", |  | ||||||
|     "includePrereleases": "Incluir versiones preliminares", |     "includePrereleases": "Incluir versiones preliminares", | ||||||
|     "fallbackToOlderReleases": "Retorceder a versiones previas", |     "fallbackToOlderReleases": "Retorceder a versiones previas", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", |     "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", |     "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", | ||||||
|     "search": "Buscar", |     "search": "Buscar", | ||||||
|     "additionalOptsFor": "Opciones Adicionales para {}", |     "additionalOptsFor": "Opciones Adicionales para {}", | ||||||
|     "supportedSourcesBelow": "Fuentes Soportadas:", |     "supportedSources": "Fuentes Soportadas", | ||||||
|     "trackOnlyInBrackets": "(Solo Seguimiento)", |     "trackOnlyInBrackets": "(Solo Seguimiento)", | ||||||
|     "searchableInBrackets": "(Soporta Búsquedas)", |     "searchableInBrackets": "(Soporta Búsquedas)", | ||||||
|     "appsString": "Aplicaciones", |     "appsString": "Aplicaciones", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "¿Eliminar Aplicación?", |         "one": "¿Eliminar Aplicación?", | ||||||
|         "other": "¿Eliminar Aplicaciones?" |         "other": "¿Eliminar Aplicaciones?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "باشه", |     "ok": "باشه", | ||||||
|     "and": "و", |     "and": "و", | ||||||
|     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", |     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", | ||||||
|     "githubPATHint": "PAT باید در این قالب باشد: username:token", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "شامل نسخه های اولیه", |     "includePrereleases": "شامل نسخه های اولیه", | ||||||
|     "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", |     "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", | ||||||
|     "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", |     "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", |     "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", | ||||||
|     "search": "جستجو کردن", |     "search": "جستجو کردن", | ||||||
|     "additionalOptsFor": "گزینه های اضافی برای {}", |     "additionalOptsFor": "گزینه های اضافی برای {}", | ||||||
|     "supportedSourcesBelow": "منابع پشتیبانی شده:", |     "supportedSources": "منابع پشتیبانی شده", | ||||||
|     "trackOnlyInBrackets": "«فقط ردیابی»", |     "trackOnlyInBrackets": "«فقط ردیابی»", | ||||||
|     "searchableInBrackets": "(قابل جستجو)", |     "searchableInBrackets": "(قابل جستجو)", | ||||||
|     "appsString": "برنامه ها", |     "appsString": "برنامه ها", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "برنامه حذف شود؟", |         "one": "برنامه حذف شود؟", | ||||||
|         "other": "برنامه ها حذف شوند؟" |         "other": "برنامه ها حذف شوند؟" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "et", |     "and": "et", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "Inclure les avant-premières", |     "includePrereleases": "Inclure les avant-premières", | ||||||
|     "fallbackToOlderReleases": "Retour aux anciennes versions", |     "fallbackToOlderReleases": "Retour aux anciennes versions", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", |     "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", |     "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", | ||||||
|     "search": "Rechercher", |     "search": "Rechercher", | ||||||
|     "additionalOptsFor": "Options supplémentaires pour {}", |     "additionalOptsFor": "Options supplémentaires pour {}", | ||||||
|     "supportedSourcesBelow": "Sources prises en charge :", |     "supportedSources": "Sources prises en charge ", | ||||||
|     "trackOnlyInBrackets": "(Suivi uniquement)", |     "trackOnlyInBrackets": "(Suivi uniquement)", | ||||||
|     "searchableInBrackets": "(Recherchable)", |     "searchableInBrackets": "(Recherchable)", | ||||||
|     "appsString": "Applications", |     "appsString": "Applications", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Supprimer l'application ?", |         "one": "Supprimer l'application ?", | ||||||
|         "other": "Supprimer les applications ?" |         "other": "Supprimer les applications ?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Oké", |     "ok": "Oké", | ||||||
|     "and": "és", |     "and": "és", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "felhasználónév:token", |  | ||||||
|     "includePrereleases": "Tartalmazza az előzetes kiadásokat", |     "includePrereleases": "Tartalmazza az előzetes kiadásokat", | ||||||
|     "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", |     "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", | ||||||
|     "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", |     "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Keresés (csak egyes források)", |     "searchSomeSourcesLabel": "Keresés (csak egyes források)", | ||||||
|     "search": "Keresés", |     "search": "Keresés", | ||||||
|     "additionalOptsFor": "További lehetőségek a következőhöz: {}", |     "additionalOptsFor": "További lehetőségek a következőhöz: {}", | ||||||
|     "supportedSourcesBelow": "Támogatott források:", |     "supportedSources": "Támogatott források", | ||||||
|     "trackOnlyInBrackets": "(Csak nyomonkövetés)", |     "trackOnlyInBrackets": "(Csak nyomonkövetés)", | ||||||
|     "searchableInBrackets": "(Kereshető)", |     "searchableInBrackets": "(Kereshető)", | ||||||
|     "appsString": "Appok", |     "appsString": "Appok", | ||||||
| @@ -87,7 +85,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", | ||||||
| @@ -137,7 +135,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:", | ||||||
| @@ -204,7 +202,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.", | ||||||
| @@ -239,15 +237,19 @@ | |||||||
|     "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 APK Link Filter by Regular Expression (Default '.apk$')", |     "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')", | ||||||
|     "appsPossiblyUpdated": "App Updates Attempted", |     "appsPossiblyUpdated": "App frissítési kísérlet", | ||||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", |     "appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben", | ||||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", |     "xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.", | ||||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", |     "backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.", | ||||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", |     "backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.", | ||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Ellenőrizze a „legújabb” címkét", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "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?" | ||||||
| @@ -297,7 +299,7 @@ | |||||||
|         "other": "{} és {} további alkalmazás frissítve." |         "other": "{} és {} további alkalmazás frissítve." | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesPossiblyInstalled": { |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|         "one": "{} and 1 more app may have been updated.", |         "one": "{} és 1 további alkalmazás is frissült.", | ||||||
|         "other": "{} and {} more apps may have been updated." |         "other": "{} és {} további alkalmazás is frissült." | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Va bene", |     "ok": "Va bene", | ||||||
|     "and": "e", |     "and": "e", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "nomeutente:token", |  | ||||||
|     "includePrereleases": "Includi prerelease", |     "includePrereleases": "Includi prerelease", | ||||||
|     "fallbackToOlderReleases": "Ripiega su release precedenti", |     "fallbackToOlderReleases": "Ripiega su release precedenti", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", |     "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", |     "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", | ||||||
|     "search": "Cerca", |     "search": "Cerca", | ||||||
|     "additionalOptsFor": "Opzioni aggiuntive per {}", |     "additionalOptsFor": "Opzioni aggiuntive per {}", | ||||||
|     "supportedSourcesBelow": "Fonti supportate:", |     "supportedSources": "Fonti supportate", | ||||||
|     "trackOnlyInBrackets": "(Solo-Monitoraggio)", |     "trackOnlyInBrackets": "(Solo-Monitoraggio)", | ||||||
|     "searchableInBrackets": "(ricercabile)", |     "searchableInBrackets": "(ricercabile)", | ||||||
|     "appsString": "App", |     "appsString": "App", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Rimuovere l'app?", |         "one": "Rimuovere l'app?", | ||||||
|         "other": "Rimuovere le app?" |         "other": "Rimuovere le app?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "OK", |     "ok": "OK", | ||||||
|     "and": "と", |     "and": "と", | ||||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", |     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", |  | ||||||
|     "githubPATFormat": "ユーザー名:トークン", |  | ||||||
|     "includePrereleases": "プレリリースを含む", |     "includePrereleases": "プレリリースを含む", | ||||||
|     "fallbackToOlderReleases": "旧リリースへのフォールバック", |     "fallbackToOlderReleases": "旧リリースへのフォールバック", | ||||||
|     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", |     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "検索 (一部ソースのみ)", |     "searchSomeSourcesLabel": "検索 (一部ソースのみ)", | ||||||
|     "search": "検索", |     "search": "検索", | ||||||
|     "additionalOptsFor": "{}の追加オプション", |     "additionalOptsFor": "{}の追加オプション", | ||||||
|     "supportedSourcesBelow": "対応するソース:", |     "supportedSources": "対応するソース", | ||||||
|     "trackOnlyInBrackets": "(追跡のみ)", |     "trackOnlyInBrackets": "(追跡のみ)", | ||||||
|     "searchableInBrackets": "(検索可能)", |     "searchableInBrackets": "(検索可能)", | ||||||
|     "appsString": "アプリ", |     "appsString": "アプリ", | ||||||
| @@ -250,6 +248,10 @@ | |||||||
|     "verifyLatestTag": "'latest'タグを確認する", |     "verifyLatestTag": "'latest'タグを確認する", | ||||||
|     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", |     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", | ||||||
|     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", |     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "アプリを削除しますか?", |         "one": "アプリを削除しますか?", | ||||||
|         "other": "アプリを削除しますか?" |         "other": "アプリを削除しますか?" | ||||||
|   | |||||||
| @@ -22,8 +22,6 @@ | |||||||
|     "ok": "Okej", |     "ok": "Okej", | ||||||
|     "and": "i", |     "and": "i", | ||||||
|     "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", |  | ||||||
|     "githubPATFormat": "użytkownik:token", |  | ||||||
|     "includePrereleases": "Uwzględnij wersje wstępne", |     "includePrereleases": "Uwzględnij wersje wstępne", | ||||||
|     "fallbackToOlderReleases": "Powracaj do starszych wersji", |     "fallbackToOlderReleases": "Powracaj do starszych wersji", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", |     "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", | ||||||
| @@ -52,7 +50,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", |     "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", | ||||||
|     "search": "Szukaj", |     "search": "Szukaj", | ||||||
|     "additionalOptsFor": "Dodatkowe opcje dla {}", |     "additionalOptsFor": "Dodatkowe opcje dla {}", | ||||||
|     "supportedSourcesBelow": "Obsługiwane źródła:", |     "supportedSources": "Obsługiwane źródła", | ||||||
|     "trackOnlyInBrackets": "(tylko obserwowane)", |     "trackOnlyInBrackets": "(tylko obserwowane)", | ||||||
|     "searchableInBrackets": "(Wyszukiwalne)", |     "searchableInBrackets": "(Wyszukiwalne)", | ||||||
|     "appsString": "Aplikacje", |     "appsString": "Aplikacje", | ||||||
| @@ -255,6 +253,10 @@ | |||||||
|     "verifyLatestTag": "Zweryfikuj najnowszy tag", |     "verifyLatestTag": "Zweryfikuj najnowszy tag", | ||||||
|     "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)", |     "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", |     "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Usunąć aplikację?", |         "one": "Usunąć aplikację?", | ||||||
|         "few": "Usunąć aplikacje?", |         "few": "Usunąć aplikacje?", | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Окей", |     "ok": "Окей", | ||||||
|     "and": "и", |     "and": "и", | ||||||
|     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", |     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", | ||||||
|     "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен", |  | ||||||
|     "githubPATFormat": "имя_пользователя:токен", |  | ||||||
|     "includePrereleases": "Включить предварительные релизы", |     "includePrereleases": "Включить предварительные релизы", | ||||||
|     "fallbackToOlderReleases": "Откатиться к более старым версиям", |     "fallbackToOlderReleases": "Откатиться к более старым версиям", | ||||||
|     "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", |     "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", |     "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", | ||||||
|     "search": "Поиск", |     "search": "Поиск", | ||||||
|     "additionalOptsFor": "Дополнительные опции для {}", |     "additionalOptsFor": "Дополнительные опции для {}", | ||||||
|     "supportedSourcesBelow": "Поддерживаемые источники:", |     "supportedSources": "Поддерживаемые источники", | ||||||
|     "trackOnlyInBrackets": "(Только для отслеживания)", |     "trackOnlyInBrackets": "(Только для отслеживания)", | ||||||
|     "searchableInBrackets": "(Поиск)", |     "searchableInBrackets": "(Поиск)", | ||||||
|     "appsString": "Приложения", |     "appsString": "Приложения", | ||||||
| @@ -249,6 +247,10 @@ | |||||||
|     "verifyLatestTag": "Verify the 'latest' tag", |     "verifyLatestTag": "Verify the 'latest' tag", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Удалить приложение?", |         "one": "Удалить приложение?", | ||||||
|         "other": "Удалить приложения?" |         "other": "Удалить приложения?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "好的", |     "ok": "好的", | ||||||
|     "and": "和", |     "and": "和", | ||||||
|     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", |     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", | ||||||
|     "githubPATHint": "个人访问令牌必须为“username:token”的格式", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "包含预发行版", |     "includePrereleases": "包含预发行版", | ||||||
|     "fallbackToOlderReleases": "将旧发行版作为备选", |     "fallbackToOlderReleases": "将旧发行版作为备选", | ||||||
|     "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", |     "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", |     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", | ||||||
|     "search": "搜索", |     "search": "搜索", | ||||||
|     "additionalOptsFor": "{} 的更多选项", |     "additionalOptsFor": "{} 的更多选项", | ||||||
|     "supportedSourcesBelow": "支持的来源:", |     "supportedSources": "支持的来源", | ||||||
|     "trackOnlyInBrackets": "(仅追踪)", |     "trackOnlyInBrackets": "(仅追踪)", | ||||||
|     "searchableInBrackets": "(可搜索)", |     "searchableInBrackets": "(可搜索)", | ||||||
|     "appsString": "应用列表", |     "appsString": "应用列表", | ||||||
| @@ -250,6 +248,10 @@ | |||||||
|     "verifyLatestTag": "验证“Latest”标签", |     "verifyLatestTag": "验证“Latest”标签", | ||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|  |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "是否删除应用?", |         "one": "是否删除应用?", | ||||||
|         "other": "是否删除应用?" |         "other": "是否删除应用?" | ||||||
|   | |||||||
| @@ -25,12 +25,16 @@ class APKCombo extends AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Map<String, String> get requestHeaders => { |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|         "User-Agent": "curl/8.0.1", |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|         "Accept": "*/*", |       bool forAPKDownload = false}) async { | ||||||
|         "Connection": "keep-alive", |     return { | ||||||
|         "Host": "$host" |       "User-Agent": "curl/8.0.1", | ||||||
|       }; |       "Accept": "*/*", | ||||||
|  |       "Connection": "keep-alive", | ||||||
|  |       "Host": "$host" | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { |   Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { | ||||||
|     var res = await sourceRequest('$standardUrl/download/apk'); |     var res = await sourceRequest('$standardUrl/download/apk'); | ||||||
|   | |||||||
| @@ -3,6 +3,21 @@ import 'package:html/parser.dart'; | |||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | parseDateTimeMMMddCommayyyy(String? dateString) { | ||||||
|  |   DateTime? releaseDate; | ||||||
|  |   try { | ||||||
|  |     releaseDate = dateString != null | ||||||
|  |         ? DateFormat('MMM dd, yyyy').parse(dateString) | ||||||
|  |         : null; | ||||||
|  |     releaseDate = dateString != null && releaseDate == null | ||||||
|  |         ? DateFormat('MMMM dd, yyyy').parse(dateString) | ||||||
|  |         : releaseDate; | ||||||
|  |   } catch (err) { | ||||||
|  |     // ignore | ||||||
|  |   } | ||||||
|  |   return releaseDate; | ||||||
|  | } | ||||||
|  |  | ||||||
| class APKPure extends AppSource { | class APKPure extends AppSource { | ||||||
|   APKPure() { |   APKPure() { | ||||||
|     host = 'apkpure.com'; |     host = 'apkpure.com'; | ||||||
| @@ -47,17 +62,7 @@ class APKPure extends AppSource { | |||||||
|       } |       } | ||||||
|       String? dateString = |       String? dateString = | ||||||
|           html.querySelector('span.info-other span.date')?.text.trim(); |           html.querySelector('span.info-other span.date')?.text.trim(); | ||||||
|       DateTime? releaseDate; |       DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString); | ||||||
|       try { |  | ||||||
|         releaseDate = dateString != null |  | ||||||
|             ? DateFormat('MMM dd, yyyy').parse(dateString) |  | ||||||
|             : null; |  | ||||||
|         releaseDate = dateString != null && releaseDate == null |  | ||||||
|             ? DateFormat('MMMM dd, yyyy').parse(dateString) |  | ||||||
|             : null; |  | ||||||
|       } catch (err) { |  | ||||||
|         // ignore |  | ||||||
|       } |  | ||||||
|       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; |       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; | ||||||
|       List<MapEntry<String, String>> apkUrls = [ |       List<MapEntry<String, String>> apkUrls = [ | ||||||
|         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') |         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') | ||||||
| @@ -70,11 +75,13 @@ class APKPure extends AppSource { | |||||||
|           Uri.parse(standardUrl).pathSegments.reversed.last; |           Uri.parse(standardUrl).pathSegments.reversed.last; | ||||||
|       String appName = |       String appName = | ||||||
|           html.querySelector('h1.info-title')?.text.trim() ?? appId; |           html.querySelector('h1.info-title')?.text.trim() ?? appId; | ||||||
|       String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml |       String? changeLog = htmlChangelog | ||||||
|           .trim().replaceAll("<br>", "  \n"); |           .querySelector("div.whats-new-info p:not(.date)") | ||||||
|  |           ?.innerHtml | ||||||
|  |           .trim() | ||||||
|  |           .replaceAll("<br>", "  \n"); | ||||||
|       return APKDetails(version, apkUrls, AppNames(author, appName), |       return APKDetails(version, apkUrls, AppNames(author, appName), | ||||||
|           releaseDate: releaseDate, |           releaseDate: releaseDate, changeLog: changeLog); | ||||||
|           changeLog: changeLog); |  | ||||||
|     } else { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | import 'dart:convert'; | ||||||
|  |  | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:obtainium/custom_errors.dart'; | ||||||
|  | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | class Aptoide extends AppSource { | ||||||
|  |   Aptoide() { | ||||||
|  |     host = 'aptoide.com'; | ||||||
|  |     name = tr('Aptoide'); | ||||||
|  |     allowSubDomains = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String sourceSpecificStandardizeURL(String url) { | ||||||
|  |     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||||
|  |     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||||
|  |     if (match == null) { | ||||||
|  |       throw InvalidURLError(name); | ||||||
|  |     } | ||||||
|  |     return url.substring(0, match.end); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<String?> tryInferringAppId(String standardUrl, | ||||||
|  |       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||||
|  |     return (await getAppDetailsJSON(standardUrl))['package']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async { | ||||||
|  |     var res = await sourceRequest(standardUrl); | ||||||
|  |     if (res.statusCode != 200) { | ||||||
|  |       throw getObtainiumHttpError(res); | ||||||
|  |     } | ||||||
|  |     var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body); | ||||||
|  |     String? id; | ||||||
|  |     if (idMatch != null) { | ||||||
|  |       id = res.body.substring(idMatch.start + 12, idMatch.end); | ||||||
|  |     } else { | ||||||
|  |       throw NoReleasesError(); | ||||||
|  |     } | ||||||
|  |     var res2 = | ||||||
|  |         await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id'); | ||||||
|  |     if (res2.statusCode != 200) { | ||||||
|  |       throw getObtainiumHttpError(res); | ||||||
|  |     } | ||||||
|  |     return jsonDecode(res2.body)?['nodes']?['meta']?['data']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<APKDetails> getLatestAPKDetails( | ||||||
|  |     String standardUrl, | ||||||
|  |     Map<String, dynamic> additionalSettings, | ||||||
|  |   ) async { | ||||||
|  |     var appDetails = await getAppDetailsJSON(standardUrl); | ||||||
|  |     String appName = appDetails['name'] ?? tr('app'); | ||||||
|  |     String author = appDetails['developer']?['name'] ?? name; | ||||||
|  |     String? dateStr = appDetails['updated']; | ||||||
|  |     String? version = appDetails['file']?['vername']; | ||||||
|  |     String? apkUrl = appDetails['file']?['path']; | ||||||
|  |     if (version == null) { | ||||||
|  |       throw NoVersionError(); | ||||||
|  |     } | ||||||
|  |     if (apkUrl == null) { | ||||||
|  |       throw NoAPKError(); | ||||||
|  |     } | ||||||
|  |     DateTime? relDate; | ||||||
|  |     if (dateStr != null) { | ||||||
|  |       relDate = DateTime.parse(dateStr); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return APKDetails( | ||||||
|  |         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||||
|  |         releaseDate: relDate); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ import 'dart:convert'; | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package: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 | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  | import 'dart:io'; | ||||||
| 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:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| @@ -21,20 +22,6 @@ class GitHub extends AppSource { | |||||||
|           label: tr('githubPATLabel'), |           label: tr('githubPATLabel'), | ||||||
|           password: true, |           password: true, | ||||||
|           required: false, |           required: false, | ||||||
|           additionalValidators: [ |  | ||||||
|             (value) { |  | ||||||
|               if (value != null && value.trim().isNotEmpty) { |  | ||||||
|                 if (value |  | ||||||
|                         .split(':') |  | ||||||
|                         .where((element) => element.trim().isNotEmpty) |  | ||||||
|                         .length != |  | ||||||
|                     2) { |  | ||||||
|                   return tr('githubPATHint'); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|               return null; |  | ||||||
|             } |  | ||||||
|           ], |  | ||||||
|           hint: tr('githubPATFormat'), |           hint: tr('githubPATFormat'), | ||||||
|           belowWidgets: [ |           belowWidgets: [ | ||||||
|             const SizedBox( |             const SizedBox( | ||||||
| @@ -169,26 +156,53 @@ class GitHub extends AppSource { | |||||||
|     return url.substring(0, match.end); |     return url.substring(0, match.end); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> getCredentialPrefixIfAny( |   @override | ||||||
|       Map<String, dynamic> additionalSettings) async { |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |       bool forAPKDownload = false}) async { | ||||||
|  |     var token = await getTokenIfAny(additionalSettings); | ||||||
|  |     var headers = <String, String>{}; | ||||||
|  |     if (token != null) { | ||||||
|  |       headers[HttpHeaders.authorizationHeader] = 'Token $token'; | ||||||
|  |     } | ||||||
|  |     if (forAPKDownload == true) { | ||||||
|  |       headers[HttpHeaders.acceptHeader] = 'application/octet-stream'; | ||||||
|  |     } | ||||||
|  |     if (headers.isNotEmpty) { | ||||||
|  |       return headers; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async { | ||||||
|     SettingsProvider settingsProvider = SettingsProvider(); |     SettingsProvider settingsProvider = SettingsProvider(); | ||||||
|     await settingsProvider.initializeSettings(); |     await settingsProvider.initializeSettings(); | ||||||
|     var sourceConfig = |     var sourceConfig = | ||||||
|         await getSourceConfigValues(additionalSettings, settingsProvider); |         await getSourceConfigValues(additionalSettings, settingsProvider); | ||||||
|     String? creds = sourceConfig['github-creds']; |     String? creds = sourceConfig['github-creds']; | ||||||
|     return creds != null && creds.isNotEmpty ? '$creds@' : ''; |     if (creds != null) { | ||||||
|  |       var userNameEndIndex = creds.indexOf(':'); | ||||||
|  |       if (userNameEndIndex > 0) { | ||||||
|  |         creds = creds.substring( | ||||||
|  |             userNameEndIndex + 1); // For old username-included token inputs | ||||||
|  |       } | ||||||
|  |       return creds; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<String?> getSourceNote() async { |   Future<String?> getSourceNote() async { | ||||||
|     if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) { |     if (!hostChanged && (await getTokenIfAny({})) == null) { | ||||||
|       return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; |       return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => |   Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => | ||||||
|       'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host'; |       'https://api.$host'; | ||||||
|  |  | ||||||
|   Future<String> convertStandardUrlToAPIUrl( |   Future<String> convertStandardUrlToAPIUrl( | ||||||
|           String standardUrl, Map<String, dynamic> additionalSettings) async => |           String standardUrl, Map<String, dynamic> additionalSettings) async => | ||||||
| @@ -238,9 +252,8 @@ class GitHub extends AppSource { | |||||||
|       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => |       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => | ||||||
|           (release['assets'] as List<dynamic>?) |           (release['assets'] as List<dynamic>?) | ||||||
|               ?.map((e) { |               ?.map((e) { | ||||||
|                 return e['name'] != null && e['browser_download_url'] != null |                 return e['name'] != null && e['url'] != null | ||||||
|                     ? MapEntry(e['name'] as String, |                     ? MapEntry(e['name'] as String, e['url'] as String) | ||||||
|                         e['browser_download_url'] as String) |  | ||||||
|                     : const MapEntry('', ''); |                     : const MapEntry('', ''); | ||||||
|               }) |               }) | ||||||
|               .where((element) => element.key.toLowerCase().endsWith('.apk')) |               .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.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/components/generated_form.dart'; | ||||||
| @@ -18,7 +19,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'; | ||||||
| @@ -109,6 +110,26 @@ class HTML extends AppSource { | |||||||
|             hint: '([0-9]+\.)*[0-9]+/\$', |             hint: '([0-9]+\.)*[0-9]+/\$', | ||||||
|             required: false, |             required: false, | ||||||
|             additionalValidators: [(value) => regExValidator(value)]) |             additionalValidators: [(value) => regExValidator(value)]) | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         GeneratedFormTextField('versionExtractionRegEx', | ||||||
|  |             label: tr('versionExtractionRegEx'), | ||||||
|  |             required: false, | ||||||
|  |             additionalValidators: [(value) => regExValidator(value)]), | ||||||
|  |         GeneratedFormTextField('matchGroupToUse', | ||||||
|  |             label: tr('matchGroupToUse'), | ||||||
|  |             required: false, | ||||||
|  |             hint: '1', | ||||||
|  |             textInputType: const TextInputType.numberWithOptions(), | ||||||
|  |             additionalValidators: [ | ||||||
|  |               (value) { | ||||||
|  |                 value ??= '1'; | ||||||
|  |                 if (int.tryParse(value) == null) { | ||||||
|  |                   return tr('invalidInput'); | ||||||
|  |                 } | ||||||
|  |                 return null; | ||||||
|  |               } | ||||||
|  |             ]) | ||||||
|       ] |       ] | ||||||
|     ]; |     ]; | ||||||
|     overrideVersionDetectionFormDefault('noVersionDetection', |     overrideVersionDetectionFormDefault('noVersionDetection', | ||||||
| @@ -116,11 +137,14 @@ class HTML extends AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   // TODO: implement requestHeaders choice, hardcoded for now |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|   Map<String, String>? get requestHeaders => { |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|         "User-Agent": |       bool forAPKDownload = false}) async { | ||||||
|             "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" |     return { | ||||||
|       }; |       "User-Agent": | ||||||
|  |           "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String sourceSpecificStandardizeURL(String url) { |   String sourceSpecificStandardizeURL(String url) { | ||||||
| @@ -180,10 +204,23 @@ class HTML extends AppSource { | |||||||
|         throw NoReleasesError(); |         throw NoReleasesError(); | ||||||
|       } |       } | ||||||
|       var rel = links.last; |       var rel = links.last; | ||||||
|       var version = rel.hashCode.toString(); |       String? version = rel.hashCode.toString(); | ||||||
|  |       var versionExtractionRegEx = | ||||||
|  |           additionalSettings['versionExtractionRegEx'] as String?; | ||||||
|  |       if (versionExtractionRegEx?.isNotEmpty == true) { | ||||||
|  |         var match = RegExp(versionExtractionRegEx!).allMatches(rel); | ||||||
|  |         if (match.isEmpty) { | ||||||
|  |           throw NoVersionError(); | ||||||
|  |         } | ||||||
|  |         version = match.last | ||||||
|  |             .group(int.parse(additionalSettings['matchGroupToUse'] as String)); | ||||||
|  |         if (version?.isEmpty == true) { | ||||||
|  |           throw NoVersionError(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       List<String> apkUrls = |       List<String> apkUrls = | ||||||
|           [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); |           [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); | ||||||
|       return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(), |       return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(), | ||||||
|           AppNames(uri.host, tr('app'))); |           AppNames(uri.host, tr('app'))); | ||||||
|     } else { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
|   | |||||||
| @@ -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); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:html/parser.dart'; | ||||||
|  | import 'package:obtainium/app_sources/apkpure.dart'; | ||||||
|  | import 'package:obtainium/custom_errors.dart'; | ||||||
|  | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | class Uptodown extends AppSource { | ||||||
|  |   Uptodown() { | ||||||
|  |     host = 'uptodown.com'; | ||||||
|  |     allowSubDomains = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String sourceSpecificStandardizeURL(String url) { | ||||||
|  |     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||||
|  |     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||||
|  |     if (match == null) { | ||||||
|  |       throw InvalidURLError(name); | ||||||
|  |     } | ||||||
|  |     return '${url.substring(0, match.end)}/android/download'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<String?> tryInferringAppId(String standardUrl, | ||||||
|  |       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||||
|  |     return (await getAppDetailsFromPage(standardUrl))['appId']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async { | ||||||
|  |     var res = await sourceRequest(standardUrl); | ||||||
|  |     if (res.statusCode != 200) { | ||||||
|  |       throw getObtainiumHttpError(res); | ||||||
|  |     } | ||||||
|  |     var html = parse(res.body); | ||||||
|  |     String? version = html.querySelector('div.version')?.innerHtml; | ||||||
|  |     String? apkUrl = | ||||||
|  |         html.querySelector('#detail-download-button')?.attributes['data-url']; | ||||||
|  |     String? name = html.querySelector('#detail-app-name')?.innerHtml.trim(); | ||||||
|  |     String? author = html.querySelector('#author-link')?.innerHtml.trim(); | ||||||
|  |     var detailElements = html.querySelectorAll('#technical-information td'); | ||||||
|  |     String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim(); | ||||||
|  |     String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim(); | ||||||
|  |     return Map.fromEntries([ | ||||||
|  |       MapEntry('version', version), | ||||||
|  |       MapEntry('apkUrl', apkUrl), | ||||||
|  |       MapEntry('appId', appId), | ||||||
|  |       MapEntry('name', name), | ||||||
|  |       MapEntry('author', author), | ||||||
|  |       MapEntry('dateStr', dateStr) | ||||||
|  |     ]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<APKDetails> getLatestAPKDetails( | ||||||
|  |     String standardUrl, | ||||||
|  |     Map<String, dynamic> additionalSettings, | ||||||
|  |   ) async { | ||||||
|  |     var appDetails = await getAppDetailsFromPage(standardUrl); | ||||||
|  |     var version = appDetails['version']; | ||||||
|  |     var apkUrl = appDetails['apkUrl']; | ||||||
|  |     var appId = appDetails['appId']; | ||||||
|  |     if (version == null) { | ||||||
|  |       throw NoVersionError(); | ||||||
|  |     } | ||||||
|  |     if (apkUrl == null) { | ||||||
|  |       throw NoAPKError(); | ||||||
|  |     } | ||||||
|  |     if (appId == null) { | ||||||
|  |       throw NoReleasesError(); | ||||||
|  |     } | ||||||
|  |     String appName = appDetails['name'] ?? tr('app'); | ||||||
|  |     String author = appDetails['author'] ?? name; | ||||||
|  |     String? dateStr = appDetails['dateStr']; | ||||||
|  |     DateTime? relDate; | ||||||
|  |     if (dateStr != null) { | ||||||
|  |       relDate = parseDateTimeMMMddCommayyyy(dateStr); | ||||||
|  |     } | ||||||
|  |     return APKDetails( | ||||||
|  |         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||||
|  |         releaseDate: relDate); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -12,7 +12,12 @@ class VLC extends AppSource { | |||||||
|   get dwUrlBase => 'https://get.$host/vlc-android/'; |   get dwUrlBase => 'https://get.$host/vlc-android/'; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Map<String, String>? get requestHeaders => HTML().requestHeaders; |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |           {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |           bool forAPKDownload = false}) => | ||||||
|  |       HTML().getRequestHeaders( | ||||||
|  |           additionalSettings: additionalSettings, | ||||||
|  |           forAPKDownload: forAPKDownload); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String sourceSpecificStandardizeURL(String url) { |   String sourceSpecificStandardizeURL(String url) { | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem { | |||||||
|   late int max; |   late int max; | ||||||
|   late String? hint; |   late String? hint; | ||||||
|   late bool password; |   late bool password; | ||||||
|  |   late TextInputType? textInputType; | ||||||
|  |  | ||||||
|   GeneratedFormTextField(String key, |   GeneratedFormTextField(String key, | ||||||
|       {String label = 'Input', |       {String label = 'Input', | ||||||
| @@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem { | |||||||
|       this.required = true, |       this.required = true, | ||||||
|       this.max = 1, |       this.max = 1, | ||||||
|       this.hint, |       this.hint, | ||||||
|       this.password = false}) |       this.password = false, | ||||||
|  |       this.textInputType}) | ||||||
|       : super(key, |       : super(key, | ||||||
|             label: label, |             label: label, | ||||||
|             belowWidgets: belowWidgets, |             belowWidgets: belowWidgets, | ||||||
| @@ -144,7 +146,8 @@ Color generateRandomLightColor() { | |||||||
|   // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 |   // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 | ||||||
|   final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); |   final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); | ||||||
|   // Map RBG values from 0-1 to 0-255: |   // Map RBG values from 0-1 to 0-255: | ||||||
|   final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); |   final List<int> rgbValues = | ||||||
|  |       rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); | ||||||
|   return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); |   return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|         if (formItem is GeneratedFormTextField) { |         if (formItem is GeneratedFormTextField) { | ||||||
|           final formFieldKey = GlobalKey<FormFieldState>(); |           final formFieldKey = GlobalKey<FormFieldState>(); | ||||||
|           return TextFormField( |           return TextFormField( | ||||||
|  |             keyboardType: formItem.textInputType, | ||||||
|             obscureText: formItem.password, |             obscureText: formItem.password, | ||||||
|             autocorrect: !formItem.password, |             autocorrect: !formItem.password, | ||||||
|             enableSuggestions: !formItem.password, |             enableSuggestions: !formItem.password, | ||||||
| @@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|                     }) ?? |                     }) ?? | ||||||
|                     [const SizedBox.shrink()], |                     [const SizedBox.shrink()], | ||||||
|                 (values[widget.items[r][e].key] |                 (values[widget.items[r][e].key] | ||||||
|                 as Map<String, MapEntry<int, bool>>?) |                                 as Map<String, MapEntry<int, bool>>?) | ||||||
|                     ?.values |                             ?.values | ||||||
|                     .where((e) => e.value) |                             .where((e) => e.value) | ||||||
|                     .length == 1 |                             .length == | ||||||
|  |                         1 | ||||||
|                     ? Padding( |                     ? Padding( | ||||||
|                     padding: const EdgeInsets.symmetric(horizontal: 4), |                         padding: const EdgeInsets.symmetric(horizontal: 4), | ||||||
|                     child: IconButton( |                         child: IconButton( | ||||||
|                       onPressed: () { |                           onPressed: () { | ||||||
|                         setState(() { |                             setState(() { | ||||||
|                           var temp = values[widget.items[r][e].key] |                               var temp = values[widget.items[r][e].key] | ||||||
|                               as Map<String, MapEntry<int, bool>>; |                                   as Map<String, MapEntry<int, bool>>; | ||||||
|                           // get selected category str where bool is true |                               // get selected category str where bool is true | ||||||
|                           final oldEntry = temp.entries.firstWhere((entry) => entry.value.value); |                               final oldEntry = temp.entries | ||||||
|                           // generate new color, ensure it is not the same |                                   .firstWhere((entry) => entry.value.value); | ||||||
|                           int newColor = oldEntry.value.key; |                               // generate new color, ensure it is not the same | ||||||
|                           while(oldEntry.value.key == newColor) { |                               int newColor = oldEntry.value.key; | ||||||
|                             newColor = generateRandomLightColor().value; |                               while (oldEntry.value.key == newColor) { | ||||||
|                           } |                                 newColor = generateRandomLightColor().value; | ||||||
|                           // Update entry with new color, remain selected |                               } | ||||||
|                           temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value)); |                               // Update entry with new color, remain selected | ||||||
|                           values[widget.items[r][e].key] = temp; |                               temp.update(oldEntry.key, | ||||||
|                           someValueChanged(); |                                   (old) => MapEntry(newColor, old.value)); | ||||||
|                         }); |                               values[widget.items[r][e].key] = temp; | ||||||
|                       }, |                               someValueChanged(); | ||||||
|                       icon: const Icon(Icons.format_color_fill_rounded), |                             }); | ||||||
|                       visualDensity: VisualDensity.compact, |                           }, | ||||||
|                       tooltip: tr('colour'), |                           icon: const Icon(Icons.format_color_fill_rounded), | ||||||
|                     )) |                           visualDensity: VisualDensity.compact, | ||||||
|  |                           tooltip: tr('colour'), | ||||||
|  |                         )) | ||||||
|                     : const SizedBox.shrink(), |                     : const SizedBox.shrink(), | ||||||
|                 (values[widget.items[r][e].key] |                 (values[widget.items[r][e].key] | ||||||
|                                 as Map<String, MapEntry<int, bool>>?) |                                 as Map<String, MapEntry<int, bool>>?) | ||||||
|   | |||||||
| @@ -19,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.14.5'; | const String currentVersion = '0.14.11'; | ||||||
| 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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource { | |||||||
|  |  | ||||||
|   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( |   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( | ||||||
|       String username, int page) async { |       String username, int page) async { | ||||||
|     Response res = await get(Uri.parse( |     Response res = await get( | ||||||
|         'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page')); |         Uri.parse( | ||||||
|  |             'https://api.github.com/users/$username/starred?per_page=100&page=$page'), | ||||||
|  |         headers: await GitHub().getRequestHeaders()); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       Map<String, List<String>> urlsWithDescriptions = {}; |       Map<String, List<String>> urlsWithDescriptions = {}; | ||||||
|       for (var e in (jsonDecode(res.body) as List<dynamic>)) { |       for (var e in (jsonDecode(res.body) as List<dynamic>)) { | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart'; | |||||||
| import 'package:obtainium/pages/import_export.dart'; | import 'package:obtainium/pages/import_export.dart'; | ||||||
| import 'package:obtainium/pages/settings.dart'; | import 'package:obtainium/pages/settings.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
|  | import 'package:obtainium/providers/notifications_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| @@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); |     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||||
|     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); |     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); | ||||||
|  |     NotificationsProvider notificationsProvider = | ||||||
|  |         context.read<NotificationsProvider>(); | ||||||
|  |  | ||||||
|     bool doingSomething = gettingAppInfo || searching; |     bool doingSomething = gettingAppInfo || searching; | ||||||
|  |  | ||||||
| @@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); |                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); | ||||||
|             // ignore: use_build_context_synchronously |             // ignore: use_build_context_synchronously | ||||||
|             var downloadedArtifact = await appsProvider.downloadApp( |             var downloadedArtifact = await appsProvider.downloadApp( | ||||||
|                 app, globalNavigatorKey.currentContext); |                 app, globalNavigatorKey.currentContext, | ||||||
|  |                 notificationsProvider: notificationsProvider); | ||||||
|             DownloadedApk? downloadedFile; |             DownloadedApk? downloadedFile; | ||||||
|             DownloadedXApkDir? downloadedDir; |             DownloadedXApkDir? downloadedDir; | ||||||
|             if (downloadedArtifact is DownloadedApk) { |             if (downloadedArtifact is DownloadedApk) { | ||||||
| @@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |             crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             children: [ |             children: [ | ||||||
|               const SizedBox( |  | ||||||
|                 height: 48, |  | ||||||
|               ), |  | ||||||
|               Text( |               Text( | ||||||
|                 tr('supportedSourcesBelow'), |                 tr('supportedSources'), | ||||||
|  |                 style: const TextStyle(fontWeight: FontWeight.bold), | ||||||
|               ), |               ), | ||||||
|               const SizedBox( |               const SizedBox( | ||||||
|                 height: 8, |                 height: 16, | ||||||
|               ), |               ), | ||||||
|               ...sourceProvider.sources |               ...sourceProvider.sources | ||||||
|                   .map((e) => GestureDetector( |                   .map((e) => GestureDetector( | ||||||
| @@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                   : const SizedBox(); |                                   : const SizedBox(); | ||||||
|                             }, |                             }, | ||||||
|                             future: pickedSource?.getSourceNote()), |                             future: pickedSource?.getSourceNote()), | ||||||
|                       const SizedBox( |                       SizedBox( | ||||||
|                         height: 16, |                         height: pickedSource != null ? 16 : 96, | ||||||
|                       ), |                       ), | ||||||
|                       if (pickedSource != null) |                       if (pickedSource != null) getAdditionalOptsCol(), | ||||||
|                         getAdditionalOptsCol() |                       if (pickedSource == null) | ||||||
|                       else |                         const Divider( | ||||||
|                         getSourcesListWidget(), |                           height: 48, | ||||||
|                       const SizedBox( |                         ), | ||||||
|                         height: 8, |                       if (pickedSource == null) getSourcesListWidget(), | ||||||
|  |                       SizedBox( | ||||||
|  |                         height: pickedSource != null ? 8 : 2, | ||||||
|                       ), |                       ), | ||||||
|                     ])), |                     ])), | ||||||
|           ) |           ) | ||||||
|   | |||||||
| @@ -449,33 +449,44 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               : const SizedBox.shrink(), |               : const SizedBox.shrink(), | ||||||
|           GestureDetector( |           GestureDetector( | ||||||
|               onTap: showChangesFn, |               onTap: showChangesFn, | ||||||
|               child: Column( |               child: Container( | ||||||
|                 mainAxisAlignment: MainAxisAlignment.center, |                   decoration: BoxDecoration( | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.end, |                       borderRadius: BorderRadius.circular(12), | ||||||
|                 children: [ |                       color: Theme.of(context).primaryColor.withAlpha( | ||||||
|                   Row(mainAxisSize: MainAxisSize.min, children: [ |                           (settingsProvider.highlightTouchTargets && | ||||||
|                     Container( |                                   showChangesFn != null) | ||||||
|                         constraints: BoxConstraints( |                               ? 20 | ||||||
|                             maxWidth: MediaQuery.of(context).size.width / 4), |                               : 0)), | ||||||
|                         child: Text(getVersionText(index), |                   padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), | ||||||
|                             overflow: TextOverflow.ellipsis, |                   child: Column( | ||||||
|                             textAlign: TextAlign.end)), |                     mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                   ]), |                     crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                   Row( |  | ||||||
|                     mainAxisSize: MainAxisSize.min, |  | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Text( |                       Row(mainAxisSize: MainAxisSize.min, children: [ | ||||||
|                         getChangesButtonString(index, showChangesFn != null), |                         Container( | ||||||
|                         style: TextStyle( |                             constraints: BoxConstraints( | ||||||
|                             fontStyle: FontStyle.italic, |                                 maxWidth: | ||||||
|                             decoration: showChangesFn != null |                                     MediaQuery.of(context).size.width / 4), | ||||||
|                                 ? TextDecoration.underline |                             child: Text(getVersionText(index), | ||||||
|                                 : TextDecoration.none), |                                 overflow: TextOverflow.ellipsis, | ||||||
|                       ) |                                 textAlign: TextAlign.end)), | ||||||
|  |                       ]), | ||||||
|  |                       Row( | ||||||
|  |                         mainAxisSize: MainAxisSize.min, | ||||||
|  |                         children: [ | ||||||
|  |                           Text( | ||||||
|  |                             getChangesButtonString( | ||||||
|  |                                 index, showChangesFn != null), | ||||||
|  |                             style: TextStyle( | ||||||
|  |                                 fontStyle: FontStyle.italic, | ||||||
|  |                                 decoration: showChangesFn != null | ||||||
|  |                                     ? TextDecoration.underline | ||||||
|  |                                     : TextDecoration.none), | ||||||
|  |                           ) | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ))) | ||||||
|                 ], |  | ||||||
|               )) |  | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -484,6 +484,21 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                                           }) |                                           }) | ||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|  |                             height16, | ||||||
|  |                             Row( | ||||||
|  |                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                               children: [ | ||||||
|  |                                 Flexible( | ||||||
|  |                                     child: Text(tr('highlightTouchTargets'))), | ||||||
|  |                                 Switch( | ||||||
|  |                                     value: | ||||||
|  |                                         settingsProvider.highlightTouchTargets, | ||||||
|  |                                     onChanged: (value) { | ||||||
|  |                                       settingsProvider.highlightTouchTargets = | ||||||
|  |                                           value; | ||||||
|  |                                     }) | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|                             height32, |                             height32, | ||||||
|                             Text( |                             Text( | ||||||
|                               tr('categories'), |                               tr('categories'), | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  | import 'dart:math'; | ||||||
|  | import 'package:http/http.dart' as http; | ||||||
|  |  | ||||||
| import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | ||||||
| import 'package:android_intent_plus/flag.dart'; | import 'package:android_intent_plus/flag.dart'; | ||||||
| @@ -116,16 +118,19 @@ moveStrToEnd(List<String> arr, String str, {String? strB}) { | |||||||
|   return arr; |   return arr; | ||||||
| } | } | ||||||
|  |  | ||||||
| moveStrToEndMapEntryWithCount( | List<MapEntry<String, int>> moveStrToEndMapEntryWithCount( | ||||||
|     List<MapEntry<String, int>> arr, MapEntry<String, int> str, |     List<MapEntry<String, int>> arr, MapEntry<String, int> str, | ||||||
|     {MapEntry<String, int>? strB}) { |     {MapEntry<String, int>? strB}) { | ||||||
|   MapEntry<String, int>? temp; |   MapEntry<String, int>? temp; | ||||||
|   arr.removeWhere((element) { |   arr.removeWhere((element) { | ||||||
|     bool res = element.key == str.key || element.key == strB?.key; |     bool resA = element.key == str.key; | ||||||
|     if (res) { |     bool resB = element.key == strB?.key; | ||||||
|       temp = element; |     if (resA) { | ||||||
|  |       temp = str; | ||||||
|  |     } else if (resB) { | ||||||
|  |       temp = strB; | ||||||
|     } |     } | ||||||
|     return res; |     return resA || resB; | ||||||
|   }); |   }); | ||||||
|   if (temp != null) { |   if (temp != null) { | ||||||
|     arr = [...arr, temp!]; |     arr = [...arr, temp!]; | ||||||
| @@ -211,7 +216,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|     if (headers != null) { |     if (headers != null) { | ||||||
|       req.headers.addAll(headers); |       req.headers.addAll(headers); | ||||||
|     } |     } | ||||||
|     var client = Client(); |     var client = http.Client(); | ||||||
|     StreamedResponse response = await client.send(req); |     StreamedResponse response = await client.send(req); | ||||||
|     String ext = |     String ext = | ||||||
|         response.headers['content-disposition']?.split('.').last ?? 'apk'; |         response.headers['content-disposition']?.split('.').last ?? 'apk'; | ||||||
| @@ -294,9 +299,11 @@ class AppsProvider with ChangeNotifier { | |||||||
|       notificationsProvider?.cancel(notif.id); |       notificationsProvider?.cancel(notif.id); | ||||||
|       int? prevProg; |       int? prevProg; | ||||||
|       var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; |       var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; | ||||||
|  |       var headers = await source.getRequestHeaders( | ||||||
|  |           additionalSettings: app.additionalSettings, forAPKDownload: true); | ||||||
|       var downloadedFile = await downloadFileWithRetry( |       var downloadedFile = await downloadFileWithRetry( | ||||||
|           downloadUrl, fileNameNoExt, headers: source.requestHeaders, |           downloadUrl, fileNameNoExt, | ||||||
|           (double? progress) { |           headers: headers, (double? progress) { | ||||||
|         int? prog = progress?.ceil(); |         int? prog = progress?.ceil(); | ||||||
|         if (apps[app.id] != null) { |         if (apps[app.id] != null) { | ||||||
|           apps[app.id]!.downloadProgress = progress; |           apps[app.id]!.downloadProgress = progress; | ||||||
| @@ -364,6 +371,9 @@ class AppsProvider with ChangeNotifier { | |||||||
|  |  | ||||||
|   Future<bool> canInstallSilently( |   Future<bool> canInstallSilently( | ||||||
|       App app, SettingsProvider settingsProvider) async { |       App app, SettingsProvider settingsProvider) async { | ||||||
|  |     if (app.id == obtainiumId) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|     if (!settingsProvider.enableBackgroundUpdates) { |     if (!settingsProvider.enableBackgroundUpdates) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| @@ -393,7 +403,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|         (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion; |         (await getInstalledInfo(app.id))?.applicationInfo?.targetSdkVersion; | ||||||
|  |  | ||||||
|     // The OS must also be new enough and the APK should target a new enough API |     // The OS must also be new enough and the APK should target a new enough API | ||||||
|     return osInfo.version.sdkInt >= 30 && |     return osInfo.version.sdkInt >= 31 && | ||||||
|         targetSDK != null && |         targetSDK != null && | ||||||
|         targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) |         targetSDK >= // https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int) | ||||||
|             (osInfo.version.sdkInt - 3); |             (osInfo.version.sdkInt - 3); | ||||||
| @@ -1331,67 +1341,75 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | |||||||
|           (netResult != ConnectivityResult.ethernet); |           (netResult != ConnectivityResult.ethernet); | ||||||
|     } |     } | ||||||
|     // Loop through all updates and check each |     // Loop through all updates and check each | ||||||
|     for (int i = 0; i < toCheck.length; i++) { |     List<App> toNotify = []; | ||||||
|       var appId = toCheck[i].key; |     try { | ||||||
|       var retryCount = toCheck[i].value; |       for (int i = 0; i < toCheck.length; i++) { | ||||||
|       AppInMemory? app = appsProvider.apps[appId]; |         var appId = toCheck[i].key; | ||||||
|       if (app?.app.installedVersion != null) { |         var attemptCount = toCheck[i].value + 1; | ||||||
|         try { |         AppInMemory? app = appsProvider.apps[appId]; | ||||||
|           notificationsProvider.notify( |         if (app?.app.installedVersion != null) { | ||||||
|               notif = CheckingUpdatesNotification(app?.name ?? appId), |           try { | ||||||
|               cancelExisting: true); |             notificationsProvider.notify( | ||||||
|           App? newApp = await appsProvider.checkUpdate(appId); |                 notif = CheckingUpdatesNotification(app?.name ?? appId), | ||||||
|           if (newApp != null) { |                 cancelExisting: true); | ||||||
|             if (networkRestricted || |             App? newApp = await appsProvider.checkUpdate(appId); | ||||||
|                 !(await appsProvider.canInstallSilently( |             if (newApp != null) { | ||||||
|                     app!.app, settingsProvider))) { |               if (networkRestricted || | ||||||
|               notificationsProvider.notify( |                   !(await appsProvider.canInstallSilently( | ||||||
|                   UpdateNotification([newApp], id: newApp.id.hashCode - 1)); |                       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 { |             } else { | ||||||
|               toInstall.add(MapEntry(appId, 0)); |               // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) | ||||||
|  |               toCheck.removeAt(i); | ||||||
|  |               i--; | ||||||
|  |               notificationsProvider | ||||||
|  |                   .notify(ErrorCheckingUpdatesNotification(e.toString())); | ||||||
|  |             } | ||||||
|  |           } finally { | ||||||
|  |             if (notif != null) { | ||||||
|  |               notificationsProvider.cancel(notif.id); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|           if (i == (toCheck.length - 1)) { |  | ||||||
|             didCompleteChecking = true; |  | ||||||
|           } |  | ||||||
|         } catch (e) { |  | ||||||
|           // If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue checking shortly |  | ||||||
|           logs.add( |  | ||||||
|               'BG update task $taskId: Got error on checking for $appId \'${e.toString()}\'.'); |  | ||||||
|           if (retryCount < maxAttempts) { |  | ||||||
|             var remainingSeconds = e is RateLimitError |  | ||||||
|                 ? (i == 0 ? (e.remainingMinutes * 60) : (5 * 60)) |  | ||||||
|                 : e is ClientException |  | ||||||
|                     ? (15 * 60) |  | ||||||
|                     : (retryCount ^ 2); |  | ||||||
|             logs.add( |  | ||||||
|                 'BG update task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).'); |  | ||||||
|             var remainingToCheck = moveStrToEndMapEntryWithCount( |  | ||||||
|                 toCheck.sublist(i), MapEntry(appId, retryCount + 1)); |  | ||||||
|             AndroidAlarmManager.oneShot( |  | ||||||
|                 Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck, |  | ||||||
|                 params: { |  | ||||||
|                   'toCheck': remainingToCheck |  | ||||||
|                       .map((entry) => {'key': entry.key, 'value': entry.value}) |  | ||||||
|                       .toList(), |  | ||||||
|                   'toInstall': toInstall |  | ||||||
|                       .map((entry) => {'key': entry.key, 'value': entry.value}) |  | ||||||
|                       .toList(), |  | ||||||
|                 }); |  | ||||||
|             break; |  | ||||||
|           } else { |  | ||||||
|             // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue) |  | ||||||
|             toCheck.removeAt(i); |  | ||||||
|             i--; |  | ||||||
|             notificationsProvider |  | ||||||
|                 .notify(ErrorCheckingUpdatesNotification(e.toString())); |  | ||||||
|           } |  | ||||||
|         } finally { |  | ||||||
|           if (notif != null) { |  | ||||||
|             notificationsProvider.cancel(notif.id); |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |     } 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 you're done checking and found some silently installable updates, schedule another task which will run in install mode | ||||||
|     if (didCompleteChecking && toInstall.isNotEmpty) { |     if (didCompleteChecking && toInstall.isNotEmpty) { | ||||||
|   | |||||||
| @@ -348,4 +348,13 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     prefs?.setBool('showDebugOpts', val); |     prefs?.setBool('showDebugOpts', val); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   bool get highlightTouchTargets { | ||||||
|  |     return prefs?.getBool('highlightTouchTargets') ?? false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set highlightTouchTargets(bool val) { | ||||||
|  |     prefs?.setBool('highlightTouchTargets', val); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ 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'; | ||||||
| @@ -25,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart'; | |||||||
| import 'package:obtainium/app_sources/sourcehut.dart'; | import 'package:obtainium/app_sources/sourcehut.dart'; | ||||||
| import 'package:obtainium/app_sources/steammobile.dart'; | import 'package:obtainium/app_sources/steammobile.dart'; | ||||||
| import 'package:obtainium/app_sources/telegramapp.dart'; | import 'package:obtainium/app_sources/telegramapp.dart'; | ||||||
|  | import 'package:obtainium/app_sources/uptodown.dart'; | ||||||
| import 'package:obtainium/app_sources/vlc.dart'; | import 'package:obtainium/app_sources/vlc.dart'; | ||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| @@ -325,6 +327,7 @@ 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(); | ||||||
| @@ -361,15 +364,23 @@ abstract class AppSource { | |||||||
|     return url; |     return url; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, String>? get requestHeaders => null; |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |       bool forAPKDownload = false}) async { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<Response> sourceRequest(String url, |   Future<Response> sourceRequest(String url, | ||||||
|       {bool followRedirects = true}) async { |       {bool followRedirects = true, | ||||||
|  |       Map<String, dynamic> additionalSettings = | ||||||
|  |           const <String, dynamic>{}}) async { | ||||||
|  |     var requestHeaders = | ||||||
|  |         await getRequestHeaders(additionalSettings: additionalSettings); | ||||||
|     if (requestHeaders != null || followRedirects == false) { |     if (requestHeaders != null || followRedirects == false) { | ||||||
|       var req = Request('GET', Uri.parse(url)); |       var req = Request('GET', Uri.parse(url)); | ||||||
|       req.followRedirects = followRedirects; |       req.followRedirects = followRedirects; | ||||||
|       if (requestHeaders != null) { |       if (requestHeaders != null) { | ||||||
|         req.headers.addAll(requestHeaders!); |         req.headers.addAll(requestHeaders); | ||||||
|       } |       } | ||||||
|       return Response.fromStream(await Client().send(req)); |       return Response.fromStream(await Client().send(req)); | ||||||
|     } else { |     } else { | ||||||
| @@ -517,18 +528,20 @@ class SourceProvider { | |||||||
|         GitLab(), |         GitLab(), | ||||||
|         Codeberg(), |         Codeberg(), | ||||||
|         FDroid(), |         FDroid(), | ||||||
|         IzzyOnDroid(), |  | ||||||
|         FDroidRepo(), |         FDroidRepo(), | ||||||
|         Jenkins(), |         IzzyOnDroid(), | ||||||
|         SourceForge(), |         SourceForge(), | ||||||
|         SourceHut(), |         SourceHut(), | ||||||
|         APKMirror(), |  | ||||||
|         APKPure(), |         APKPure(), | ||||||
|  |         Aptoide(), | ||||||
|  |         Uptodown(), | ||||||
|  |         APKMirror(), | ||||||
|         HuaweiAppGallery(), |         HuaweiAppGallery(), | ||||||
|  |         Jenkins(), | ||||||
|         // 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(), | ||||||
|         VLC(), // As of 2023-08-26 this site randomly messes up the 'latest' version (one minute it's 3.5.4, next minute back to 3.5.3) |         VLC(), | ||||||
|         // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date |         // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date | ||||||
|         TelegramApp(), |         TelegramApp(), | ||||||
|         SteamMobile(), |         SteamMobile(), | ||||||
| @@ -554,7 +567,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; | ||||||
|       } |       } | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -46,10 +46,10 @@ packages: | |||||||
|     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: | ||||||
| @@ -206,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: | ||||||
| @@ -538,18 +538,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: permission_handler |       name: permission_handler | ||||||
|       sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" |       sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.4.3" |     version: "10.4.5" | ||||||
|   permission_handler_android: |   permission_handler_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_android |       name: permission_handler_android | ||||||
|       sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3 |       sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.3.4" |     version: "10.3.6" | ||||||
|   permission_handler_apple: |   permission_handler_apple: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -562,10 +562,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_platform_interface |       name: permission_handler_platform_interface | ||||||
|       sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" |       sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.11.3" |     version: "3.11.5" | ||||||
|   permission_handler_windows: |   permission_handler_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -783,18 +783,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: url_launcher |       name: url_launcher | ||||||
|       sha256: "38d8e783681bc342e92dc9799cbe4421d08c4d210a67ee9d61d0f7310491a465" |       sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.1.13" |     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: ef7e34951ffa963fb7a65928deeb38d40fb3c5975baf93c1d631341ff7f2650a |       sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.39" |     version: "6.1.0" | ||||||
|   url_launcher_ios: |   url_launcher_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -823,10 +823,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: url_launcher_platform_interface |       name: url_launcher_platform_interface | ||||||
|       sha256: "4d0dae953f80dc06fa24c58d0f8381302139c22c2dad301417787ad96f5f73bd" |       sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.4" |     version: "2.1.5" | ||||||
|   url_launcher_web: |   url_launcher_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -879,18 +879,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_android |       name: webview_flutter_android | ||||||
|       sha256: "8545719bbf06f5c4b850f0d5f86da1fe837b1953c56b9af579a26be73627c98d" |       sha256: "9427774649fd3c8b7ff53523051395d13aed2ca355822b822e6493d79f5fc05a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.9.4" |     version: "3.10.0" | ||||||
|   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: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b" |       sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.5.1" |     version: "2.6.0" | ||||||
|   webview_flutter_wkwebview: |   webview_flutter_wkwebview: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -17,10 +17,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # 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.14.5+197 # When changing this, update the tag in main() accordingly | version: 0.14.11+203 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=3.0.0 <4.0.0' | ||||||
|  |  | ||||||
| # Dependencies specify other packages that your package needs in order to work. | # Dependencies specify other packages that your package needs in order to work. | ||||||
| # To automatically upgrade your package dependencies to the latest versions | # To automatically upgrade your package dependencies to the latest versions | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user