mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-04 07:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			v0.14.7-be
			...
			v0.14.12-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c1e64f111e | ||
| 
						 | 
					b2af8448fd | ||
| 
						 | 
					8f44338e76 | ||
| 
						 | 
					e4a55abcb3 | ||
| 
						 | 
					d7348b4973 | ||
| 
						 | 
					09421230f2 | ||
| 
						 | 
					4596e32258 | ||
| 
						 | 
					4dc007a4f6 | ||
| 
						 | 
					c53a156969 | ||
| 
						 | 
					94bd0774fb | ||
| 
						 | 
					b178b1d780 | ||
| 
						 | 
					cbc840378c | ||
| 
						 | 
					aa7989c16d | ||
| 
						 | 
					85f9336804 | ||
| 
						 | 
					d66be3ecda | ||
| 
						 | 
					c08e05bd6c | ||
| 
						 | 
					e08ab89fd4 | ||
| 
						 | 
					8ba0a0a776 | ||
| 
						 | 
					73ed0cea88 | ||
| 
						 | 
					58a378d212 | ||
| 
						 | 
					553307ba70 | ||
| 
						 | 
					78f73a9049 | ||
| 
						 | 
					abc69e7a0e | ||
| 
						 | 
					503914dbce | 
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -2,31 +2,31 @@
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Something isn't working right.
 | 
			
		||||
title: ''
 | 
			
		||||
labels: bug, To Check
 | 
			
		||||
labels: bug, to check
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Prerequisites**
 | 
			
		||||
Please ensure your request is not part of an existing issue.
 | 
			
		||||
<!-- Please ensure your request is not part of an existing issue. -->
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
A clear and concise description of what the bug is.
 | 
			
		||||
<!-- A clear and concise description of what the bug is. -->
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
<!-- Steps to reproduce the behavior:
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Tap on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
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.
 | 
			
		||||
<!-- 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]
 | 
			
		||||
 - 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.
 | 
			
		||||
<!-- Add any other context about the problem here. -->
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							@@ -2,28 +2,28 @@
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest a new Source, setting, or other feature.
 | 
			
		||||
title: ''
 | 
			
		||||
labels: enhancement, To Check
 | 
			
		||||
labels: enhancement, to check
 | 
			
		||||
assignees: ''
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Prerequisites**
 | 
			
		||||
Please ensure your request is not part of an existing issue.
 | 
			
		||||
<!-- 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.
 | 
			
		||||
<!-- 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.
 | 
			
		||||
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.
 | 
			
		||||
<!-- 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).
 | 
			
		||||
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.
 | 
			
		||||
<!-- Add any other context or screenshots about the feature request here. -->
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							@@ -2,31 +2,36 @@
 | 
			
		||||
 | 
			
		||||
Get Android App Updates Directly From the Source.
 | 
			
		||||
 | 
			
		||||
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available.
 | 
			
		||||
Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.
 | 
			
		||||
 | 
			
		||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
 | 
			
		||||
 | 
			
		||||
Currently supported App sources:
 | 
			
		||||
- [GitHub](https://github.com/)
 | 
			
		||||
- [GitLab](https://gitlab.com/)
 | 
			
		||||
- [Codeberg](https://codeberg.org/)
 | 
			
		||||
- [F-Droid](https://f-droid.org/)
 | 
			
		||||
- [IzzyOnDroid](https://android.izzysoft.de/)
 | 
			
		||||
- [Mullvad](https://mullvad.net/en/)
 | 
			
		||||
- [Signal](https://signal.org/)
 | 
			
		||||
- [SourceForge](https://sourceforge.net/)
 | 
			
		||||
- [SourceHut](https://git.sr.ht/)
 | 
			
		||||
- [Aptoide](https://aptoide.com/)
 | 
			
		||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
 | 
			
		||||
- [APKPure](https://apkpure.com/)
 | 
			
		||||
- [Huawei AppGallery](https://appgallery.huawei.com/)
 | 
			
		||||
- Third Party F-Droid Repos
 | 
			
		||||
- Jenkins Jobs
 | 
			
		||||
- [Steam](https://store.steampowered.com/mobile)
 | 
			
		||||
- [Telegram App](https://telegram.org)
 | 
			
		||||
- [Neutron Code](https://neutroncode.com)
 | 
			
		||||
- "HTML" (Fallback)
 | 
			
		||||
  - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
 | 
			
		||||
- Open Source - General:
 | 
			
		||||
  - [GitHub](https://github.com/)
 | 
			
		||||
  - [GitLab](https://gitlab.com/)
 | 
			
		||||
  - [Codeberg](https://codeberg.org/)
 | 
			
		||||
  - [F-Droid](https://f-droid.org/)
 | 
			
		||||
  - Third Party F-Droid Repos
 | 
			
		||||
  - [IzzyOnDroid](https://android.izzysoft.de/)
 | 
			
		||||
  - [SourceForge](https://sourceforge.net/)
 | 
			
		||||
  - [SourceHut](https://git.sr.ht/)
 | 
			
		||||
- Other - General:
 | 
			
		||||
  - [APKPure](https://apkpure.com/)
 | 
			
		||||
  - [Aptoide](https://aptoide.com/)
 | 
			
		||||
  - [Uptodown](https://uptodown.com/)
 | 
			
		||||
  - [APKMirror](https://apkmirror.com/) (Track-Only)
 | 
			
		||||
  - [Huawei AppGallery](https://appgallery.huawei.com/)
 | 
			
		||||
  - Jenkins Jobs
 | 
			
		||||
- Open Source - App-Specific:
 | 
			
		||||
  - [Mullvad](https://mullvad.net/en/)
 | 
			
		||||
  - [Signal](https://signal.org/)
 | 
			
		||||
  - [VLC](https://videolan.org/)
 | 
			
		||||
- Other - App-Specific:
 | 
			
		||||
  - [Telegram App](https://telegram.org)
 | 
			
		||||
  - [Steam Mobile Apps](https://store.steampowered.com/mobile)
 | 
			
		||||
  - [Neutron Code](https://neutroncode.com)
 | 
			
		||||
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Ok",
 | 
			
		||||
    "and": "e",
 | 
			
		||||
    "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)",
 | 
			
		||||
    "githubPATHint": "O TAP deve estar nesse formato: usuario:token",
 | 
			
		||||
    "githubPATFormat": "usuario:token",
 | 
			
		||||
    "includePrereleases": "Incluir pré-lançamentos",
 | 
			
		||||
    "fallbackToOlderReleases": "Retornar para versões anteriores",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)",
 | 
			
		||||
    "search": "Procurar",
 | 
			
		||||
    "additionalOptsFor": "Opções Adicionais para {}",
 | 
			
		||||
    "supportedSourcesBelow": "Fontes Compatíveis:",
 | 
			
		||||
    "supportedSources": "Fontes Compatíveis",
 | 
			
		||||
    "trackOnlyInBrackets": "(Apenas Seguir)",
 | 
			
		||||
    "searchableInBrackets": "(Pesquisável)",
 | 
			
		||||
    "appsString": "Apps",
 | 
			
		||||
@@ -252,6 +250,10 @@
 | 
			
		||||
    "intermediateLinkNotFound": "Link intermediário não encontrado",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remover App?",
 | 
			
		||||
        "other": "Remover Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
   "ok": "Dobro",
 | 
			
		||||
   "and": "i",
 | 
			
		||||
   "githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)",
 | 
			
		||||
   "githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token",
 | 
			
		||||
   "githubPATFormat": "korisničko_ime:token",
 | 
			
		||||
   "includePrereleases": "Uključi preliminarna izdanja",
 | 
			
		||||
   "fallbackToOlderReleases": "Povratak na starija izdanja",
 | 
			
		||||
   "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
   "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)",
 | 
			
		||||
   "search": "Pretraživanje",
 | 
			
		||||
   "additionalOptsFor": "Dodatne opcije za {}",
 | 
			
		||||
   "supportedSourcesBelow": "Podržani izvori:",
 | 
			
		||||
   "supportedSources": "Podržani izvori",
 | 
			
		||||
   "trackOnlyInBrackets": "(Samo za praćenje)",
 | 
			
		||||
   "searchableInBrackets": "(Može se pretraživati)",
 | 
			
		||||
   "appsString": "Aplikacije",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
   "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
   "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
   "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
   "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
   "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
   "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
   "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
   "removeAppQuestion": {
 | 
			
		||||
      "one": "Želite li ukloniti aplikaciju?",
 | 
			
		||||
      "other": "Želite li ukloniti aplikacije?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Okay",
 | 
			
		||||
    "and": "und",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)",
 | 
			
		||||
    "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token",
 | 
			
		||||
    "githubPATFormat": "Benutzername:Token",
 | 
			
		||||
    "includePrereleases": "Vorabversionen einbeziehen",
 | 
			
		||||
    "fallbackToOlderReleases": "Fallback auf ältere Versionen",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)",
 | 
			
		||||
    "search": "Suchen",
 | 
			
		||||
    "additionalOptsFor": "Zusatzoptionen für {}",
 | 
			
		||||
    "supportedSourcesBelow": "Unterstützte Quellen:",
 | 
			
		||||
    "supportedSources": "Unterstützte Quellen",
 | 
			
		||||
    "trackOnlyInBrackets": "(Nur Nachverfolgen)",
 | 
			
		||||
    "searchableInBrackets": "(Durchsuchbar)",
 | 
			
		||||
    "appsString": "Apps",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Überprüfe das 'latest' Tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "App entfernen?",
 | 
			
		||||
        "other": "Apps entfernen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Okay",
 | 
			
		||||
    "and": "and",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)",
 | 
			
		||||
    "githubPATHint": "PAT must be in this format: username:token",
 | 
			
		||||
    "githubPATFormat": "username:token",
 | 
			
		||||
    "includePrereleases": "Include prereleases",
 | 
			
		||||
    "fallbackToOlderReleases": "Fallback to older releases",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Search (Some Sources Only)",
 | 
			
		||||
    "search": "Search",
 | 
			
		||||
    "additionalOptsFor": "Additional Options for {}",
 | 
			
		||||
    "supportedSourcesBelow": "Supported Sources:",
 | 
			
		||||
    "supportedSources": "Supported Sources",
 | 
			
		||||
    "trackOnlyInBrackets": "(Track-Only)",
 | 
			
		||||
    "searchableInBrackets": "(Searchable)",
 | 
			
		||||
    "appsString": "Apps",
 | 
			
		||||
@@ -252,6 +250,10 @@
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remove App?",
 | 
			
		||||
        "other": "Remove Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Correcto",
 | 
			
		||||
    "and": "y",
 | 
			
		||||
    "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
 | 
			
		||||
    "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token",
 | 
			
		||||
    "githubPATFormat": "nombre_de_usuario:token",
 | 
			
		||||
    "includePrereleases": "Incluir versiones preliminares",
 | 
			
		||||
    "fallbackToOlderReleases": "Retorceder a versiones previas",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
 | 
			
		||||
    "search": "Buscar",
 | 
			
		||||
    "additionalOptsFor": "Opciones Adicionales para {}",
 | 
			
		||||
    "supportedSourcesBelow": "Fuentes Soportadas:",
 | 
			
		||||
    "supportedSources": "Fuentes Soportadas",
 | 
			
		||||
    "trackOnlyInBrackets": "(Solo Seguimiento)",
 | 
			
		||||
    "searchableInBrackets": "(Soporta Búsquedas)",
 | 
			
		||||
    "appsString": "Aplicaciones",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "¿Eliminar Aplicación?",
 | 
			
		||||
        "other": "¿Eliminar Aplicaciones?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "باشه",
 | 
			
		||||
    "and": "و",
 | 
			
		||||
    "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)",
 | 
			
		||||
    "githubPATHint": "PAT باید در این قالب باشد: username:token",
 | 
			
		||||
    "githubPATFormat": "username:token",
 | 
			
		||||
    "includePrereleases": "شامل نسخه های اولیه",
 | 
			
		||||
    "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)",
 | 
			
		||||
    "search": "جستجو کردن",
 | 
			
		||||
    "additionalOptsFor": "گزینه های اضافی برای {}",
 | 
			
		||||
    "supportedSourcesBelow": "منابع پشتیبانی شده:",
 | 
			
		||||
    "supportedSources": "منابع پشتیبانی شده",
 | 
			
		||||
    "trackOnlyInBrackets": "«فقط ردیابی»",
 | 
			
		||||
    "searchableInBrackets": "(قابل جستجو)",
 | 
			
		||||
    "appsString": "برنامه ها",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "برنامه حذف شود؟",
 | 
			
		||||
        "other": "برنامه ها حذف شوند؟"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Okay",
 | 
			
		||||
    "and": "et",
 | 
			
		||||
    "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)",
 | 
			
		||||
    "githubPATHint": "Le JAP doit être dans ce format : username:token",
 | 
			
		||||
    "githubPATFormat": "username:token",
 | 
			
		||||
    "includePrereleases": "Inclure les avant-premières",
 | 
			
		||||
    "fallbackToOlderReleases": "Retour aux anciennes versions",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)",
 | 
			
		||||
    "search": "Rechercher",
 | 
			
		||||
    "additionalOptsFor": "Options supplémentaires pour {}",
 | 
			
		||||
    "supportedSourcesBelow": "Sources prises en charge :",
 | 
			
		||||
    "supportedSources": "Sources prises en charge ",
 | 
			
		||||
    "trackOnlyInBrackets": "(Suivi uniquement)",
 | 
			
		||||
    "searchableInBrackets": "(Recherchable)",
 | 
			
		||||
    "appsString": "Applications",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Supprimer l'application ?",
 | 
			
		||||
        "other": "Supprimer les applications ?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Oké",
 | 
			
		||||
    "and": "és",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)",
 | 
			
		||||
    "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token",
 | 
			
		||||
    "githubPATFormat": "felhasználónév:token",
 | 
			
		||||
    "includePrereleases": "Tartalmazza az előzetes kiadásokat",
 | 
			
		||||
    "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Keresés (csak egyes források)",
 | 
			
		||||
    "search": "Keresés",
 | 
			
		||||
    "additionalOptsFor": "További lehetőségek a következőhöz: {}",
 | 
			
		||||
    "supportedSourcesBelow": "Támogatott források:",
 | 
			
		||||
    "supportedSources": "Támogatott források",
 | 
			
		||||
    "trackOnlyInBrackets": "(Csak nyomonkövetés)",
 | 
			
		||||
    "searchableInBrackets": "(Kereshető)",
 | 
			
		||||
    "appsString": "Appok",
 | 
			
		||||
@@ -248,6 +246,10 @@
 | 
			
		||||
    "verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Eltávolítja az alkalmazást?",
 | 
			
		||||
        "other": "Eltávolítja az alkalmazást?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Va bene",
 | 
			
		||||
    "and": "e",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)",
 | 
			
		||||
    "githubPATHint": "PAT deve seguire questo formato: nomeutente:token",
 | 
			
		||||
    "githubPATFormat": "nomeutente:token",
 | 
			
		||||
    "includePrereleases": "Includi prerelease",
 | 
			
		||||
    "fallbackToOlderReleases": "Ripiega su release precedenti",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)",
 | 
			
		||||
    "search": "Cerca",
 | 
			
		||||
    "additionalOptsFor": "Opzioni aggiuntive per {}",
 | 
			
		||||
    "supportedSourcesBelow": "Fonti supportate:",
 | 
			
		||||
    "supportedSources": "Fonti supportate",
 | 
			
		||||
    "trackOnlyInBrackets": "(Solo-Monitoraggio)",
 | 
			
		||||
    "searchableInBrackets": "(ricercabile)",
 | 
			
		||||
    "appsString": "App",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Rimuovere l'app?",
 | 
			
		||||
        "other": "Rimuovere le app?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "OK",
 | 
			
		||||
    "and": "と",
 | 
			
		||||
    "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)",
 | 
			
		||||
    "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン",
 | 
			
		||||
    "githubPATFormat": "ユーザー名:トークン",
 | 
			
		||||
    "includePrereleases": "プレリリースを含む",
 | 
			
		||||
    "fallbackToOlderReleases": "旧リリースへのフォールバック",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "検索 (一部ソースのみ)",
 | 
			
		||||
    "search": "検索",
 | 
			
		||||
    "additionalOptsFor": "{}の追加オプション",
 | 
			
		||||
    "supportedSourcesBelow": "対応するソース:",
 | 
			
		||||
    "supportedSources": "対応するソース",
 | 
			
		||||
    "trackOnlyInBrackets": "(追跡のみ)",
 | 
			
		||||
    "searchableInBrackets": "(検索可能)",
 | 
			
		||||
    "appsString": "アプリ",
 | 
			
		||||
@@ -250,6 +248,10 @@
 | 
			
		||||
    "verifyLatestTag": "'latest'タグを確認する",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "アプリを削除しますか?",
 | 
			
		||||
        "other": "アプリを削除しますか?"
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,6 @@
 | 
			
		||||
    "ok": "Okej",
 | 
			
		||||
    "and": "i",
 | 
			
		||||
    "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)",
 | 
			
		||||
    "githubPATHint": "Wymagany format: użytkownik:token",
 | 
			
		||||
    "githubPATFormat": "użytkownik:token",
 | 
			
		||||
    "includePrereleases": "Uwzględnij wersje wstępne",
 | 
			
		||||
    "fallbackToOlderReleases": "Powracaj do starszych wersji",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych",
 | 
			
		||||
@@ -52,7 +50,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)",
 | 
			
		||||
    "search": "Szukaj",
 | 
			
		||||
    "additionalOptsFor": "Dodatkowe opcje dla {}",
 | 
			
		||||
    "supportedSourcesBelow": "Obsługiwane źródła:",
 | 
			
		||||
    "supportedSources": "Obsługiwane źródła",
 | 
			
		||||
    "trackOnlyInBrackets": "(tylko obserwowane)",
 | 
			
		||||
    "searchableInBrackets": "(Wyszukiwalne)",
 | 
			
		||||
    "appsString": "Aplikacje",
 | 
			
		||||
@@ -255,6 +253,10 @@
 | 
			
		||||
    "verifyLatestTag": "Zweryfikuj najnowszy tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Usunąć aplikację?",
 | 
			
		||||
        "few": "Usunąć aplikacje?",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "Окей",
 | 
			
		||||
    "and": "и",
 | 
			
		||||
    "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)",
 | 
			
		||||
    "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен",
 | 
			
		||||
    "githubPATFormat": "имя_пользователя:токен",
 | 
			
		||||
    "includePrereleases": "Включить предварительные релизы",
 | 
			
		||||
    "fallbackToOlderReleases": "Откатиться к более старым версиям",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)",
 | 
			
		||||
    "search": "Поиск",
 | 
			
		||||
    "additionalOptsFor": "Дополнительные опции для {}",
 | 
			
		||||
    "supportedSourcesBelow": "Поддерживаемые источники:",
 | 
			
		||||
    "supportedSources": "Поддерживаемые источники",
 | 
			
		||||
    "trackOnlyInBrackets": "(Только для отслеживания)",
 | 
			
		||||
    "searchableInBrackets": "(Поиск)",
 | 
			
		||||
    "appsString": "Приложения",
 | 
			
		||||
@@ -249,6 +247,10 @@
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Удалить приложение?",
 | 
			
		||||
        "other": "Удалить приложения?"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@
 | 
			
		||||
    "ok": "好的",
 | 
			
		||||
    "and": "和",
 | 
			
		||||
    "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)",
 | 
			
		||||
    "githubPATHint": "个人访问令牌必须为“username:token”的格式",
 | 
			
		||||
    "githubPATFormat": "username:token",
 | 
			
		||||
    "includePrereleases": "包含预发行版",
 | 
			
		||||
    "fallbackToOlderReleases": "将旧发行版作为备选",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题",
 | 
			
		||||
@@ -43,7 +41,7 @@
 | 
			
		||||
    "searchSomeSourcesLabel": "搜索(仅支持部分来源)",
 | 
			
		||||
    "search": "搜索",
 | 
			
		||||
    "additionalOptsFor": "{} 的更多选项",
 | 
			
		||||
    "supportedSourcesBelow": "支持的来源:",
 | 
			
		||||
    "supportedSources": "支持的来源",
 | 
			
		||||
    "trackOnlyInBrackets": "(仅追踪)",
 | 
			
		||||
    "searchableInBrackets": "(可搜索)",
 | 
			
		||||
    "appsString": "应用列表",
 | 
			
		||||
@@ -250,6 +248,10 @@
 | 
			
		||||
    "verifyLatestTag": "验证“Latest”标签",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "是否删除应用?",
 | 
			
		||||
        "other": "是否删除应用?"
 | 
			
		||||
 
 | 
			
		||||
@@ -25,12 +25,16 @@ class APKCombo extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, String> get requestHeaders => {
 | 
			
		||||
        "User-Agent": "curl/8.0.1",
 | 
			
		||||
        "Accept": "*/*",
 | 
			
		||||
        "Connection": "keep-alive",
 | 
			
		||||
        "Host": "$host"
 | 
			
		||||
      };
 | 
			
		||||
  Future<Map<String, String>?> getRequestHeaders(
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
 | 
			
		||||
      bool forAPKDownload = false}) async {
 | 
			
		||||
    return {
 | 
			
		||||
      "User-Agent": "curl/8.0.1",
 | 
			
		||||
      "Accept": "*/*",
 | 
			
		||||
      "Connection": "keep-alive",
 | 
			
		||||
      "Host": "$host"
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
 | 
			
		||||
    var res = await sourceRequest('$standardUrl/download/apk');
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,21 @@ import 'package:html/parser.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
parseDateTimeMMMddCommayyyy(String? dateString) {
 | 
			
		||||
  DateTime? releaseDate;
 | 
			
		||||
  try {
 | 
			
		||||
    releaseDate = dateString != null
 | 
			
		||||
        ? DateFormat('MMM dd, yyyy').parse(dateString)
 | 
			
		||||
        : null;
 | 
			
		||||
    releaseDate = dateString != null && releaseDate == null
 | 
			
		||||
        ? DateFormat('MMMM dd, yyyy').parse(dateString)
 | 
			
		||||
        : releaseDate;
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    // ignore
 | 
			
		||||
  }
 | 
			
		||||
  return releaseDate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class APKPure extends AppSource {
 | 
			
		||||
  APKPure() {
 | 
			
		||||
    host = 'apkpure.com';
 | 
			
		||||
@@ -47,17 +62,7 @@ class APKPure extends AppSource {
 | 
			
		||||
      }
 | 
			
		||||
      String? dateString =
 | 
			
		||||
          html.querySelector('span.info-other span.date')?.text.trim();
 | 
			
		||||
      DateTime? releaseDate;
 | 
			
		||||
      try {
 | 
			
		||||
        releaseDate = dateString != null
 | 
			
		||||
            ? DateFormat('MMM dd, yyyy').parse(dateString)
 | 
			
		||||
            : null;
 | 
			
		||||
        releaseDate = dateString != null && releaseDate == null
 | 
			
		||||
            ? DateFormat('MMMM dd, yyyy').parse(dateString)
 | 
			
		||||
            : null;
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        // ignore
 | 
			
		||||
      }
 | 
			
		||||
      DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
 | 
			
		||||
      String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
 | 
			
		||||
      List<MapEntry<String, String>> apkUrls = [
 | 
			
		||||
        MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
 | 
			
		||||
@@ -70,11 +75,13 @@ class APKPure extends AppSource {
 | 
			
		||||
          Uri.parse(standardUrl).pathSegments.reversed.last;
 | 
			
		||||
      String appName =
 | 
			
		||||
          html.querySelector('h1.info-title')?.text.trim() ?? appId;
 | 
			
		||||
      String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml
 | 
			
		||||
          .trim().replaceAll("<br>", "  \n");
 | 
			
		||||
      String? changeLog = htmlChangelog
 | 
			
		||||
          .querySelector("div.whats-new-info p:not(.date)")
 | 
			
		||||
          ?.innerHtml
 | 
			
		||||
          .trim()
 | 
			
		||||
          .replaceAll("<br>", "  \n");
 | 
			
		||||
      return APKDetails(version, apkUrls, AppNames(author, appName),
 | 
			
		||||
          releaseDate: releaseDate,
 | 
			
		||||
          changeLog: changeLog);
 | 
			
		||||
          releaseDate: releaseDate, changeLog: changeLog);
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -26,14 +24,10 @@ class Aptoide extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String?> tryInferringAppId(String standardUrl,
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const {}}) async {
 | 
			
		||||
    return (await getLatestAPKDetails(standardUrl, additionalSettings)).version;
 | 
			
		||||
    return (await getAppDetailsJSON(standardUrl))['package'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
  Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async {
 | 
			
		||||
    var res = await sourceRequest(standardUrl);
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
@@ -50,12 +44,20 @@ class Aptoide extends AppSource {
 | 
			
		||||
    if (res2.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var appDetails = jsonDecode(res2.body)?['nodes']?['meta']?['data'];
 | 
			
		||||
    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'];
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
@@ -71,34 +73,4 @@ class Aptoide extends AppSource {
 | 
			
		||||
        version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
 | 
			
		||||
        releaseDate: relDate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, List<String>>> search(String query,
 | 
			
		||||
      {Map<String, dynamic> querySettings = const {}}) async {
 | 
			
		||||
    Response res = await sourceRequest(
 | 
			
		||||
        'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      Map<String, List<String>> urlsWithDescriptions = {};
 | 
			
		||||
      parse(res.body).querySelectorAll('.package-header').forEach((e) {
 | 
			
		||||
        String? url = e.attributes['href'];
 | 
			
		||||
        if (url != null) {
 | 
			
		||||
          try {
 | 
			
		||||
            standardizeUrl(url);
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            url = null;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (url != null) {
 | 
			
		||||
          urlsWithDescriptions[url] = [
 | 
			
		||||
            e.querySelector('.package-name')?.text.trim() ?? '',
 | 
			
		||||
            e.querySelector('.package-summary')?.text.trim() ??
 | 
			
		||||
                tr('noDescription')
 | 
			
		||||
          ];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      return urlsWithDescriptions;
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import 'dart:convert';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
@@ -11,6 +12,12 @@ class FDroid extends AppSource {
 | 
			
		||||
    host = 'f-droid.org';
 | 
			
		||||
    name = tr('fdroid');
 | 
			
		||||
    canSearch = true;
 | 
			
		||||
    additionalSourceAppSpecificSettingFormItems = [
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSwitch('autoSelectHighestVersionCode',
 | 
			
		||||
            label: tr('autoSelectHighestVersionCode'))
 | 
			
		||||
      ]
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -37,7 +44,8 @@ class FDroid extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
      Response res, String apkUrlPrefix, String standardUrl) {
 | 
			
		||||
      Response res, String apkUrlPrefix, String standardUrl,
 | 
			
		||||
      {bool autoSelectHighestVersionCode = false}) {
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
 | 
			
		||||
      if (releases.isEmpty) {
 | 
			
		||||
@@ -47,8 +55,12 @@ class FDroid extends AppSource {
 | 
			
		||||
      if (latestVersion == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls = releases
 | 
			
		||||
          .where((element) => element['versionName'] == latestVersion)
 | 
			
		||||
      Iterable<dynamic> latestReleases =
 | 
			
		||||
          releases.where((element) => element['versionName'] == latestVersion);
 | 
			
		||||
      if (latestReleases.length > 1 && autoSelectHighestVersionCode) {
 | 
			
		||||
        latestReleases = [latestReleases.first];
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls = latestReleases
 | 
			
		||||
          .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
 | 
			
		||||
          .toList();
 | 
			
		||||
      return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
 | 
			
		||||
@@ -68,7 +80,9 @@ class FDroid extends AppSource {
 | 
			
		||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
        await sourceRequest('https://$host/api/v1/packages/$appId'),
 | 
			
		||||
        'https://$host/repo/$appId',
 | 
			
		||||
        standardUrl);
 | 
			
		||||
        standardUrl,
 | 
			
		||||
        autoSelectHighestVersionCode:
 | 
			
		||||
            additionalSettings['autoSelectHighestVersionCode'] == true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
@@ -21,20 +22,6 @@ class GitHub extends AppSource {
 | 
			
		||||
          label: tr('githubPATLabel'),
 | 
			
		||||
          password: true,
 | 
			
		||||
          required: false,
 | 
			
		||||
          additionalValidators: [
 | 
			
		||||
            (value) {
 | 
			
		||||
              if (value != null && value.trim().isNotEmpty) {
 | 
			
		||||
                if (value
 | 
			
		||||
                        .split(':')
 | 
			
		||||
                        .where((element) => element.trim().isNotEmpty)
 | 
			
		||||
                        .length !=
 | 
			
		||||
                    2) {
 | 
			
		||||
                  return tr('githubPATHint');
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              return null;
 | 
			
		||||
            }
 | 
			
		||||
          ],
 | 
			
		||||
          hint: tr('githubPATFormat'),
 | 
			
		||||
          belowWidgets: [
 | 
			
		||||
            const SizedBox(
 | 
			
		||||
@@ -169,26 +156,53 @@ class GitHub extends AppSource {
 | 
			
		||||
    return url.substring(0, match.end);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> getCredentialPrefixIfAny(
 | 
			
		||||
      Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, String>?> getRequestHeaders(
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
 | 
			
		||||
      bool forAPKDownload = false}) async {
 | 
			
		||||
    var token = await getTokenIfAny(additionalSettings);
 | 
			
		||||
    var headers = <String, String>{};
 | 
			
		||||
    if (token != null) {
 | 
			
		||||
      headers[HttpHeaders.authorizationHeader] = 'Token $token';
 | 
			
		||||
    }
 | 
			
		||||
    if (forAPKDownload == true) {
 | 
			
		||||
      headers[HttpHeaders.acceptHeader] = 'application/octet-stream';
 | 
			
		||||
    }
 | 
			
		||||
    if (headers.isNotEmpty) {
 | 
			
		||||
      return headers;
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
    SettingsProvider settingsProvider = SettingsProvider();
 | 
			
		||||
    await settingsProvider.initializeSettings();
 | 
			
		||||
    var sourceConfig =
 | 
			
		||||
        await getSourceConfigValues(additionalSettings, settingsProvider);
 | 
			
		||||
    String? creds = sourceConfig['github-creds'];
 | 
			
		||||
    return creds != null && creds.isNotEmpty ? '$creds@' : '';
 | 
			
		||||
    if (creds != null) {
 | 
			
		||||
      var userNameEndIndex = creds.indexOf(':');
 | 
			
		||||
      if (userNameEndIndex > 0) {
 | 
			
		||||
        creds = creds.substring(
 | 
			
		||||
            userNameEndIndex + 1); // For old username-included token inputs
 | 
			
		||||
      }
 | 
			
		||||
      return creds;
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String?> getSourceNote() async {
 | 
			
		||||
    if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) {
 | 
			
		||||
    if (!hostChanged && (await getTokenIfAny({})) == null) {
 | 
			
		||||
      return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}';
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async =>
 | 
			
		||||
      'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host';
 | 
			
		||||
      'https://api.$host';
 | 
			
		||||
 | 
			
		||||
  Future<String> convertStandardUrlToAPIUrl(
 | 
			
		||||
          String standardUrl, Map<String, dynamic> additionalSettings) async =>
 | 
			
		||||
@@ -238,9 +252,10 @@ class GitHub extends AppSource {
 | 
			
		||||
      List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
 | 
			
		||||
          (release['assets'] as List<dynamic>?)
 | 
			
		||||
              ?.map((e) {
 | 
			
		||||
                return e['name'] != null && e['browser_download_url'] != null
 | 
			
		||||
                return (e['name'] != null) &&
 | 
			
		||||
                        ((e['url'] ?? e['browser_download_url']) != null)
 | 
			
		||||
                    ? MapEntry(e['name'] as String,
 | 
			
		||||
                        e['browser_download_url'] as String)
 | 
			
		||||
                        (e['url'] ?? e['browser_download_url']) as String)
 | 
			
		||||
                    : const MapEntry('', '');
 | 
			
		||||
              })
 | 
			
		||||
              .where((element) => element.key.toLowerCase().endsWith('.apk'))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
@@ -18,7 +19,7 @@ String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
 | 
			
		||||
      .toList();
 | 
			
		||||
  if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
 | 
			
		||||
    return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
 | 
			
		||||
  } else if (ambiguousUrl.split('/').length == 1) {
 | 
			
		||||
  } else if (ambiguousUrl.split('/').where((e) => e.isNotEmpty).length == 1) {
 | 
			
		||||
    return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
 | 
			
		||||
  } else {
 | 
			
		||||
    return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
 | 
			
		||||
@@ -109,6 +110,26 @@ class HTML extends AppSource {
 | 
			
		||||
            hint: '([0-9]+\.)*[0-9]+/\$',
 | 
			
		||||
            required: false,
 | 
			
		||||
            additionalValidators: [(value) => regExValidator(value)])
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormTextField('versionExtractionRegEx',
 | 
			
		||||
            label: tr('versionExtractionRegEx'),
 | 
			
		||||
            required: false,
 | 
			
		||||
            additionalValidators: [(value) => regExValidator(value)]),
 | 
			
		||||
        GeneratedFormTextField('matchGroupToUse',
 | 
			
		||||
            label: tr('matchGroupToUse'),
 | 
			
		||||
            required: false,
 | 
			
		||||
            hint: '1',
 | 
			
		||||
            textInputType: const TextInputType.numberWithOptions(),
 | 
			
		||||
            additionalValidators: [
 | 
			
		||||
              (value) {
 | 
			
		||||
                value ??= '1';
 | 
			
		||||
                if (int.tryParse(value) == null) {
 | 
			
		||||
                  return tr('invalidInput');
 | 
			
		||||
                }
 | 
			
		||||
                return null;
 | 
			
		||||
              }
 | 
			
		||||
            ])
 | 
			
		||||
      ]
 | 
			
		||||
    ];
 | 
			
		||||
    overrideVersionDetectionFormDefault('noVersionDetection',
 | 
			
		||||
@@ -116,11 +137,14 @@ class HTML extends AppSource {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  // TODO: implement requestHeaders choice, hardcoded for now
 | 
			
		||||
  Map<String, String>? get requestHeaders => {
 | 
			
		||||
        "User-Agent":
 | 
			
		||||
            "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
 | 
			
		||||
      };
 | 
			
		||||
  Future<Map<String, String>?> getRequestHeaders(
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
 | 
			
		||||
      bool forAPKDownload = false}) async {
 | 
			
		||||
    return {
 | 
			
		||||
      "User-Agent":
 | 
			
		||||
          "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36"
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
@@ -180,10 +204,23 @@ class HTML extends AppSource {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      }
 | 
			
		||||
      var rel = links.last;
 | 
			
		||||
      var version = rel.hashCode.toString();
 | 
			
		||||
      String? version = rel.hashCode.toString();
 | 
			
		||||
      var versionExtractionRegEx =
 | 
			
		||||
          additionalSettings['versionExtractionRegEx'] as String?;
 | 
			
		||||
      if (versionExtractionRegEx?.isNotEmpty == true) {
 | 
			
		||||
        var match = RegExp(versionExtractionRegEx!).allMatches(rel);
 | 
			
		||||
        if (match.isEmpty) {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
        version = match.last
 | 
			
		||||
            .group(int.parse(additionalSettings['matchGroupToUse'] as String));
 | 
			
		||||
        if (version?.isEmpty == true) {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls =
 | 
			
		||||
          [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
 | 
			
		||||
      return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(),
 | 
			
		||||
      return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(),
 | 
			
		||||
          AppNames(uri.host, tr('app')));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,13 @@ import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
class IzzyOnDroid extends AppSource {
 | 
			
		||||
  late FDroid fd;
 | 
			
		||||
 | 
			
		||||
  IzzyOnDroid() {
 | 
			
		||||
    host = 'android.izzysoft.de';
 | 
			
		||||
    fd = FDroid();
 | 
			
		||||
    additionalSourceAppSpecificSettingFormItems =
 | 
			
		||||
        fd.additionalSourceAppSpecificSettingFormItems;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -20,7 +25,7 @@ class IzzyOnDroid extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<String?> tryInferringAppId(String standardUrl,
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const {}}) async {
 | 
			
		||||
    return FDroid().tryInferringAppId(standardUrl);
 | 
			
		||||
    return fd.tryInferringAppId(standardUrl);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -29,10 +34,12 @@ class IzzyOnDroid extends AppSource {
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    String? appId = await tryInferringAppId(standardUrl);
 | 
			
		||||
    return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
    return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
        await sourceRequest(
 | 
			
		||||
            'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
 | 
			
		||||
        'https://android.izzysoft.de/frepo/$appId',
 | 
			
		||||
        standardUrl);
 | 
			
		||||
        standardUrl,
 | 
			
		||||
        autoSelectHighestVersionCode:
 | 
			
		||||
            additionalSettings['autoSelectHighestVersionCode'] == true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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/';
 | 
			
		||||
 | 
			
		||||
  @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
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
 | 
			
		||||
  late int max;
 | 
			
		||||
  late String? hint;
 | 
			
		||||
  late bool password;
 | 
			
		||||
  late TextInputType? textInputType;
 | 
			
		||||
 | 
			
		||||
  GeneratedFormTextField(String key,
 | 
			
		||||
      {String label = 'Input',
 | 
			
		||||
@@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
 | 
			
		||||
      this.required = true,
 | 
			
		||||
      this.max = 1,
 | 
			
		||||
      this.hint,
 | 
			
		||||
      this.password = false})
 | 
			
		||||
      this.password = false,
 | 
			
		||||
      this.textInputType})
 | 
			
		||||
      : super(key,
 | 
			
		||||
            label: label,
 | 
			
		||||
            belowWidgets: belowWidgets,
 | 
			
		||||
@@ -144,7 +146,8 @@ Color generateRandomLightColor() {
 | 
			
		||||
  // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70
 | 
			
		||||
  final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]);
 | 
			
		||||
  // Map RBG values from 0-1 to 0-255:
 | 
			
		||||
  final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
 | 
			
		||||
  final List<int> rgbValues =
 | 
			
		||||
      rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList();
 | 
			
		||||
  return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
        if (formItem is GeneratedFormTextField) {
 | 
			
		||||
          final formFieldKey = GlobalKey<FormFieldState>();
 | 
			
		||||
          return TextFormField(
 | 
			
		||||
            keyboardType: formItem.textInputType,
 | 
			
		||||
            obscureText: formItem.password,
 | 
			
		||||
            autocorrect: !formItem.password,
 | 
			
		||||
            enableSuggestions: !formItem.password,
 | 
			
		||||
@@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                    }) ??
 | 
			
		||||
                    [const SizedBox.shrink()],
 | 
			
		||||
                (values[widget.items[r][e].key]
 | 
			
		||||
                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                    ?.values
 | 
			
		||||
                    .where((e) => e.value)
 | 
			
		||||
                    .length == 1
 | 
			
		||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                            ?.values
 | 
			
		||||
                            .where((e) => e.value)
 | 
			
		||||
                            .length ==
 | 
			
		||||
                        1
 | 
			
		||||
                    ? Padding(
 | 
			
		||||
                    padding: const EdgeInsets.symmetric(horizontal: 4),
 | 
			
		||||
                    child: IconButton(
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        setState(() {
 | 
			
		||||
                          var temp = values[widget.items[r][e].key]
 | 
			
		||||
                              as Map<String, MapEntry<int, bool>>;
 | 
			
		||||
                          // get selected category str where bool is true
 | 
			
		||||
                          final oldEntry = temp.entries.firstWhere((entry) => entry.value.value);
 | 
			
		||||
                          // generate new color, ensure it is not the same
 | 
			
		||||
                          int newColor = oldEntry.value.key;
 | 
			
		||||
                          while(oldEntry.value.key == newColor) {
 | 
			
		||||
                            newColor = generateRandomLightColor().value;
 | 
			
		||||
                          }
 | 
			
		||||
                          // Update entry with new color, remain selected
 | 
			
		||||
                          temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value));
 | 
			
		||||
                          values[widget.items[r][e].key] = temp;
 | 
			
		||||
                          someValueChanged();
 | 
			
		||||
                        });
 | 
			
		||||
                      },
 | 
			
		||||
                      icon: const Icon(Icons.format_color_fill_rounded),
 | 
			
		||||
                      visualDensity: VisualDensity.compact,
 | 
			
		||||
                      tooltip: tr('colour'),
 | 
			
		||||
                    ))
 | 
			
		||||
                        padding: const EdgeInsets.symmetric(horizontal: 4),
 | 
			
		||||
                        child: IconButton(
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            setState(() {
 | 
			
		||||
                              var temp = values[widget.items[r][e].key]
 | 
			
		||||
                                  as Map<String, MapEntry<int, bool>>;
 | 
			
		||||
                              // get selected category str where bool is true
 | 
			
		||||
                              final oldEntry = temp.entries
 | 
			
		||||
                                  .firstWhere((entry) => entry.value.value);
 | 
			
		||||
                              // generate new color, ensure it is not the same
 | 
			
		||||
                              int newColor = oldEntry.value.key;
 | 
			
		||||
                              while (oldEntry.value.key == newColor) {
 | 
			
		||||
                                newColor = generateRandomLightColor().value;
 | 
			
		||||
                              }
 | 
			
		||||
                              // Update entry with new color, remain selected
 | 
			
		||||
                              temp.update(oldEntry.key,
 | 
			
		||||
                                  (old) => MapEntry(newColor, old.value));
 | 
			
		||||
                              values[widget.items[r][e].key] = temp;
 | 
			
		||||
                              someValueChanged();
 | 
			
		||||
                            });
 | 
			
		||||
                          },
 | 
			
		||||
                          icon: const Icon(Icons.format_color_fill_rounded),
 | 
			
		||||
                          visualDensity: VisualDensity.compact,
 | 
			
		||||
                          tooltip: tr('colour'),
 | 
			
		||||
                        ))
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                (values[widget.items[r][e].key]
 | 
			
		||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.14.7';
 | 
			
		||||
const String currentVersion = '0.14.12';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource {
 | 
			
		||||
 | 
			
		||||
  Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
 | 
			
		||||
      String username, int page) async {
 | 
			
		||||
    Response res = await get(Uri.parse(
 | 
			
		||||
        'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page'));
 | 
			
		||||
    Response res = await get(
 | 
			
		||||
        Uri.parse(
 | 
			
		||||
            'https://api.github.com/users/$username/starred?per_page=100&page=$page'),
 | 
			
		||||
        headers: await GitHub().getRequestHeaders());
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      Map<String, List<String>> urlsWithDescriptions = {};
 | 
			
		||||
      for (var e in (jsonDecode(res.body) as List<dynamic>)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart';
 | 
			
		||||
import 'package:obtainium/pages/import_export.dart';
 | 
			
		||||
import 'package:obtainium/pages/settings.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/notifications_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
@@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    AppsProvider appsProvider = context.read<AppsProvider>();
 | 
			
		||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
			
		||||
    NotificationsProvider notificationsProvider =
 | 
			
		||||
        context.read<NotificationsProvider>();
 | 
			
		||||
 | 
			
		||||
    bool doingSomething = gettingAppInfo || searching;
 | 
			
		||||
 | 
			
		||||
@@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value);
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
            var downloadedArtifact = await appsProvider.downloadApp(
 | 
			
		||||
                app, globalNavigatorKey.currentContext);
 | 
			
		||||
                app, globalNavigatorKey.currentContext,
 | 
			
		||||
                notificationsProvider: notificationsProvider);
 | 
			
		||||
            DownloadedApk? downloadedFile;
 | 
			
		||||
            DownloadedXApkDir? downloadedDir;
 | 
			
		||||
            if (downloadedArtifact is DownloadedApk) {
 | 
			
		||||
@@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              const SizedBox(
 | 
			
		||||
                height: 48,
 | 
			
		||||
              ),
 | 
			
		||||
              Text(
 | 
			
		||||
                tr('supportedSourcesBelow'),
 | 
			
		||||
                tr('supportedSources'),
 | 
			
		||||
                style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(
 | 
			
		||||
                height: 8,
 | 
			
		||||
                height: 16,
 | 
			
		||||
              ),
 | 
			
		||||
              ...sourceProvider.sources
 | 
			
		||||
                  .map((e) => GestureDetector(
 | 
			
		||||
@@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                                  : const SizedBox();
 | 
			
		||||
                            },
 | 
			
		||||
                            future: pickedSource?.getSourceNote()),
 | 
			
		||||
                      const SizedBox(
 | 
			
		||||
                        height: 16,
 | 
			
		||||
                      SizedBox(
 | 
			
		||||
                        height: pickedSource != null ? 16 : 96,
 | 
			
		||||
                      ),
 | 
			
		||||
                      if (pickedSource != null)
 | 
			
		||||
                        getAdditionalOptsCol()
 | 
			
		||||
                      else
 | 
			
		||||
                        getSourcesListWidget(),
 | 
			
		||||
                      const SizedBox(
 | 
			
		||||
                        height: 8,
 | 
			
		||||
                      if (pickedSource != null) getAdditionalOptsCol(),
 | 
			
		||||
                      if (pickedSource == null)
 | 
			
		||||
                        const Divider(
 | 
			
		||||
                          height: 48,
 | 
			
		||||
                        ),
 | 
			
		||||
                      if (pickedSource == null) getSourcesListWidget(),
 | 
			
		||||
                      SizedBox(
 | 
			
		||||
                        height: pickedSource != null ? 8 : 2,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ])),
 | 
			
		||||
          )
 | 
			
		||||
 
 | 
			
		||||
@@ -449,33 +449,46 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
              : const SizedBox.shrink(),
 | 
			
		||||
          GestureDetector(
 | 
			
		||||
              onTap: showChangesFn,
 | 
			
		||||
              child: Column(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
                children: [
 | 
			
		||||
                  Row(mainAxisSize: MainAxisSize.min, children: [
 | 
			
		||||
                    Container(
 | 
			
		||||
                        constraints: BoxConstraints(
 | 
			
		||||
                            maxWidth: MediaQuery.of(context).size.width / 4),
 | 
			
		||||
                        child: Text(getVersionText(index),
 | 
			
		||||
                            overflow: TextOverflow.ellipsis,
 | 
			
		||||
                            textAlign: TextAlign.end)),
 | 
			
		||||
                  ]),
 | 
			
		||||
                  Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              child: Container(
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                      borderRadius: BorderRadius.circular(12),
 | 
			
		||||
                      color: settingsProvider.highlightTouchTargets &&
 | 
			
		||||
                              showChangesFn != null
 | 
			
		||||
                          ? (Theme.of(context).brightness == Brightness.light
 | 
			
		||||
                                  ? Theme.of(context).primaryColor
 | 
			
		||||
                                  : Theme.of(context).primaryColorLight)
 | 
			
		||||
                              .withAlpha(20)
 | 
			
		||||
                          : null),
 | 
			
		||||
                  padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        getChangesButtonString(index, showChangesFn != null),
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                            fontStyle: FontStyle.italic,
 | 
			
		||||
                            decoration: showChangesFn != null
 | 
			
		||||
                                ? TextDecoration.underline
 | 
			
		||||
                                : TextDecoration.none),
 | 
			
		||||
                      )
 | 
			
		||||
                      Row(mainAxisSize: MainAxisSize.min, children: [
 | 
			
		||||
                        Container(
 | 
			
		||||
                            constraints: BoxConstraints(
 | 
			
		||||
                                maxWidth:
 | 
			
		||||
                                    MediaQuery.of(context).size.width / 4),
 | 
			
		||||
                            child: Text(getVersionText(index),
 | 
			
		||||
                                overflow: TextOverflow.ellipsis,
 | 
			
		||||
                                textAlign: TextAlign.end)),
 | 
			
		||||
                      ]),
 | 
			
		||||
                      Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            getChangesButtonString(
 | 
			
		||||
                                index, showChangesFn != null),
 | 
			
		||||
                            style: TextStyle(
 | 
			
		||||
                                fontStyle: FontStyle.italic,
 | 
			
		||||
                                decoration: showChangesFn != null
 | 
			
		||||
                                    ? TextDecoration.underline
 | 
			
		||||
                                    : TextDecoration.none),
 | 
			
		||||
                          )
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ))
 | 
			
		||||
                  )))
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
                            Text(
 | 
			
		||||
                              tr('categories'),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import 'dart:async';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
 | 
			
		||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
 | 
			
		||||
import 'package:android_intent_plus/flag.dart';
 | 
			
		||||
@@ -215,7 +216,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    if (headers != null) {
 | 
			
		||||
      req.headers.addAll(headers);
 | 
			
		||||
    }
 | 
			
		||||
    var client = Client();
 | 
			
		||||
    var client = http.Client();
 | 
			
		||||
    StreamedResponse response = await client.send(req);
 | 
			
		||||
    String ext =
 | 
			
		||||
        response.headers['content-disposition']?.split('.').last ?? 'apk';
 | 
			
		||||
@@ -298,9 +299,11 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      notificationsProvider?.cancel(notif.id);
 | 
			
		||||
      int? prevProg;
 | 
			
		||||
      var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}';
 | 
			
		||||
      var headers = await source.getRequestHeaders(
 | 
			
		||||
          additionalSettings: app.additionalSettings, forAPKDownload: true);
 | 
			
		||||
      var downloadedFile = await downloadFileWithRetry(
 | 
			
		||||
          downloadUrl, fileNameNoExt, headers: source.requestHeaders,
 | 
			
		||||
          (double? progress) {
 | 
			
		||||
          downloadUrl, fileNameNoExt,
 | 
			
		||||
          headers: headers, (double? progress) {
 | 
			
		||||
        int? prog = progress?.ceil();
 | 
			
		||||
        if (apps[app.id] != null) {
 | 
			
		||||
          apps[app.id]!.downloadProgress = progress;
 | 
			
		||||
 
 | 
			
		||||
@@ -348,4 +348,13 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    prefs?.setBool('showDebugOpts', val);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get highlightTouchTargets {
 | 
			
		||||
    return prefs?.getBool('highlightTouchTargets') ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set highlightTouchTargets(bool val) {
 | 
			
		||||
    prefs?.setBool('highlightTouchTargets', val);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/sourcehut.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/steammobile.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/telegramapp.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/uptodown.dart';
 | 
			
		||||
import 'package:obtainium/app_sources/vlc.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
@@ -363,15 +364,23 @@ abstract class AppSource {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Map<String, String>? get requestHeaders => null;
 | 
			
		||||
  Future<Map<String, String>?> getRequestHeaders(
 | 
			
		||||
      {Map<String, dynamic> additionalSettings = const <String, dynamic>{},
 | 
			
		||||
      bool forAPKDownload = false}) async {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<Response> sourceRequest(String url,
 | 
			
		||||
      {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) {
 | 
			
		||||
      var req = Request('GET', Uri.parse(url));
 | 
			
		||||
      req.followRedirects = followRedirects;
 | 
			
		||||
      if (requestHeaders != null) {
 | 
			
		||||
        req.headers.addAll(requestHeaders!);
 | 
			
		||||
        req.headers.addAll(requestHeaders);
 | 
			
		||||
      }
 | 
			
		||||
      return Response.fromStream(await Client().send(req));
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -519,15 +528,16 @@ class SourceProvider {
 | 
			
		||||
        GitLab(),
 | 
			
		||||
        Codeberg(),
 | 
			
		||||
        FDroid(),
 | 
			
		||||
        IzzyOnDroid(),
 | 
			
		||||
        FDroidRepo(),
 | 
			
		||||
        Jenkins(),
 | 
			
		||||
        IzzyOnDroid(),
 | 
			
		||||
        SourceForge(),
 | 
			
		||||
        SourceHut(),
 | 
			
		||||
        Aptoide(),
 | 
			
		||||
        APKMirror(),
 | 
			
		||||
        APKPure(),
 | 
			
		||||
        Aptoide(),
 | 
			
		||||
        Uptodown(),
 | 
			
		||||
        APKMirror(),
 | 
			
		||||
        HuaweiAppGallery(),
 | 
			
		||||
        Jenkins(),
 | 
			
		||||
        // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
 | 
			
		||||
        Mullvad(),
 | 
			
		||||
        Signal(),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -538,18 +538,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler
 | 
			
		||||
      sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81"
 | 
			
		||||
      sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "10.4.3"
 | 
			
		||||
    version: "10.4.5"
 | 
			
		||||
  permission_handler_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_android
 | 
			
		||||
      sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3
 | 
			
		||||
      sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "10.3.4"
 | 
			
		||||
    version: "10.3.6"
 | 
			
		||||
  permission_handler_apple:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -562,10 +562,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_platform_interface
 | 
			
		||||
      sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9"
 | 
			
		||||
      sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.11.3"
 | 
			
		||||
    version: "3.11.5"
 | 
			
		||||
  permission_handler_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -879,18 +879,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_android
 | 
			
		||||
      sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa"
 | 
			
		||||
      sha256: "9427774649fd3c8b7ff53523051395d13aed2ca355822b822e6493d79f5fc05a"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.9.5"
 | 
			
		||||
    version: "3.10.0"
 | 
			
		||||
  webview_flutter_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_platform_interface
 | 
			
		||||
      sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b"
 | 
			
		||||
      sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.5.1"
 | 
			
		||||
    version: "2.6.0"
 | 
			
		||||
  webview_flutter_wkwebview:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    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
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 0.14.7+199 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.14.12+204 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=2.18.2 <3.0.0'
 | 
			
		||||
  sdk: '>=3.0.0 <4.0.0'
 | 
			
		||||
 | 
			
		||||
# Dependencies specify other packages that your package needs in order to work.
 | 
			
		||||
# To automatically upgrade your package dependencies to the latest versions
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user