mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-31 05:23:28 +01:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			v0.14.6-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 | 
							
								
								
									
										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. | ||||
|  | ||||
| Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||
| Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||
|  | ||||
| Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | ||||
|  | ||||
| Currently supported App sources: | ||||
| - [GitHub](https://github.com/) | ||||
| - [GitLab](https://gitlab.com/) | ||||
| - [Codeberg](https://codeberg.org/) | ||||
| - [F-Droid](https://f-droid.org/) | ||||
| - [IzzyOnDroid](https://android.izzysoft.de/) | ||||
| - [Mullvad](https://mullvad.net/en/) | ||||
| - [Signal](https://signal.org/) | ||||
| - [SourceForge](https://sourceforge.net/) | ||||
| - [SourceHut](https://git.sr.ht/) | ||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||
| - [APKPure](https://apkpure.com/) | ||||
| - [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", | ||||
| @@ -87,7 +85,7 @@ | ||||
|     "author": "Szerző", | ||||
|     "upToDateApps": "Naprakész appok", | ||||
|     "nonInstalledApps": "Nem telepített appok", | ||||
|     "importExport": "Importálás/Exportálás", | ||||
|     "importExport": "Import/Export", | ||||
|     "settings": "Beállítások", | ||||
|     "exportedTo": "Exportálva ide {}", | ||||
|     "obtainiumExport": "Obtainium Adat Exportálás", | ||||
| @@ -137,7 +135,7 @@ | ||||
|     "share": "Megosztás", | ||||
|     "appNotFound": "App nem található", | ||||
|     "obtainiumExportHyphenatedLowercase": "obtainium-export", | ||||
|     "pickAnAPK": "Válasszon egy APK-ot", | ||||
|     "pickAnAPK": "Válasszon egy APK-t", | ||||
|     "appHasMoreThanOnePackage": "A(z) {} egynél több csomaggal rendelkezik:", | ||||
|     "deviceSupportsXArch": "Eszköze támogatja a {} CPU architektúrát.", | ||||
|     "deviceSupportsFollowingArchs": "Az eszköze a következő CPU architektúrákat támogatja:", | ||||
| @@ -204,7 +202,7 @@ | ||||
|     "copiedToClipboard": "Másolva a vágólapra", | ||||
|     "storagePermissionDenied": "Tárhely engedély megtagadva", | ||||
|     "selectedCategorizeWarning": "Ez felváltja a kiválasztott alkalmazások meglévő kategória-beállításait.", | ||||
|     "filterAPKsByRegEx": "Az APK-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", | ||||
|     "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.", | ||||
| @@ -239,15 +237,19 @@ | ||||
|     "gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.", | ||||
|     "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett", | ||||
|     "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel", | ||||
|     "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", | ||||
|     "appsPossiblyUpdated": "App Updates Attempted", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", | ||||
|     "xWasPossiblyUpdatedToY": "{} may have been updated to {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", | ||||
|     "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", | ||||
|     "verifyLatestTag": "Verify the 'latest' tag", | ||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||
|     "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')", | ||||
|     "appsPossiblyUpdated": "App frissítési kísérlet", | ||||
|     "appsPossiblyUpdatedNotifDescription": "Értesíti a felhasználót, hogy egy vagy több alkalmazás frissítése lehetséges a háttérben", | ||||
|     "xWasPossiblyUpdatedToY": "{} frissítve lehet erre {}.", | ||||
|     "backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.", | ||||
|     "backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.", | ||||
|     "verifyLatestTag": "Ellenőrizze a „legújabb” címkét", | ||||
|     "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)", | ||||
|     "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n", | ||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||
|     "versionExtractionRegEx": "Version Extraction RegEx", | ||||
|     "matchGroupToUse": "Match Group to Use", | ||||
|     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||
|     "removeAppQuestion": { | ||||
|         "one": "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." | ||||
|     }, | ||||
|     "xAndNMoreUpdatesPossiblyInstalled": { | ||||
|         "one": "{} and 1 more app may have been updated.", | ||||
|         "other": "{} and {} more apps may have been updated." | ||||
|         "one": "{} és 1 további alkalmazás is frissült.", | ||||
|         "other": "{} és {} további alkalmazás is frissült." | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								lib/app_sources/aptoide.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| class Aptoide extends AppSource { | ||||
|   Aptoide() { | ||||
|     host = 'aptoide.com'; | ||||
|     name = tr('Aptoide'); | ||||
|     allowSubDomains = true; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String sourceSpecificStandardizeURL(String url) { | ||||
|     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||
|     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||
|     if (match == null) { | ||||
|       throw InvalidURLError(name); | ||||
|     } | ||||
|     return url.substring(0, match.end); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<String?> tryInferringAppId(String standardUrl, | ||||
|       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||
|     return (await getAppDetailsJSON(standardUrl))['package']; | ||||
|   } | ||||
|  | ||||
|   Future<Map<String, dynamic>> getAppDetailsJSON(String standardUrl) async { | ||||
|     var res = await sourceRequest(standardUrl); | ||||
|     if (res.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body); | ||||
|     String? id; | ||||
|     if (idMatch != null) { | ||||
|       id = res.body.substring(idMatch.start + 12, idMatch.end); | ||||
|     } else { | ||||
|       throw NoReleasesError(); | ||||
|     } | ||||
|     var res2 = | ||||
|         await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id'); | ||||
|     if (res2.statusCode != 200) { | ||||
|       throw getObtainiumHttpError(res); | ||||
|     } | ||||
|     return jsonDecode(res2.body)?['nodes']?['meta']?['data']; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<APKDetails> getLatestAPKDetails( | ||||
|     String standardUrl, | ||||
|     Map<String, dynamic> additionalSettings, | ||||
|   ) async { | ||||
|     var appDetails = await getAppDetailsJSON(standardUrl); | ||||
|     String appName = appDetails['name'] ?? tr('app'); | ||||
|     String author = appDetails['developer']?['name'] ?? name; | ||||
|     String? dateStr = appDetails['updated']; | ||||
|     String? version = appDetails['file']?['vername']; | ||||
|     String? apkUrl = appDetails['file']?['path']; | ||||
|     if (version == null) { | ||||
|       throw NoVersionError(); | ||||
|     } | ||||
|     if (apkUrl == null) { | ||||
|       throw NoAPKError(); | ||||
|     } | ||||
|     DateTime? relDate; | ||||
|     if (dateStr != null) { | ||||
|       relDate = DateTime.parse(dateStr); | ||||
|     } | ||||
|  | ||||
|     return APKDetails( | ||||
|         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||
|         releaseDate: relDate); | ||||
|   } | ||||
| } | ||||
| @@ -3,6 +3,7 @@ import 'dart:convert'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/components/generated_form.dart'; | ||||
| import 'package:obtainium/custom_errors.dart'; | ||||
| import 'package:obtainium/providers/source_provider.dart'; | ||||
|  | ||||
| @@ -11,6 +12,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,8 @@ 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 | ||||
|                     ? MapEntry(e['name'] as String, | ||||
|                         e['browser_download_url'] as String) | ||||
|                 return e['name'] != null && e['url'] != null | ||||
|                     ? MapEntry(e['name'] as String, e['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.6'; | ||||
| const String currentVersion = '0.14.11'; | ||||
| 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,44 @@ 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: Theme.of(context).primaryColor.withAlpha( | ||||
|                           (settingsProvider.highlightTouchTargets && | ||||
|                                   showChangesFn != null) | ||||
|                               ? 20 | ||||
|                               : 0)), | ||||
|                   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(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:html/dom.dart'; | ||||
| import 'package:http/http.dart'; | ||||
| import 'package:obtainium/app_sources/apkmirror.dart'; | ||||
| import 'package:obtainium/app_sources/apkpure.dart'; | ||||
| import 'package:obtainium/app_sources/aptoide.dart'; | ||||
| import 'package:obtainium/app_sources/codeberg.dart'; | ||||
| import 'package:obtainium/app_sources/fdroid.dart'; | ||||
| import 'package:obtainium/app_sources/fdroidrepo.dart'; | ||||
| @@ -25,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'; | ||||
| @@ -325,6 +327,7 @@ abstract class AppSource { | ||||
|   bool enforceTrackOnly = false; | ||||
|   bool changeLogIfAnyIsMarkDown = true; | ||||
|   bool appIdInferIsOptional = false; | ||||
|   bool allowSubDomains = false; | ||||
|  | ||||
|   AppSource() { | ||||
|     name = runtimeType.toString(); | ||||
| @@ -361,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 { | ||||
| @@ -517,18 +528,20 @@ class SourceProvider { | ||||
|         GitLab(), | ||||
|         Codeberg(), | ||||
|         FDroid(), | ||||
|         IzzyOnDroid(), | ||||
|         FDroidRepo(), | ||||
|         Jenkins(), | ||||
|         IzzyOnDroid(), | ||||
|         SourceForge(), | ||||
|         SourceHut(), | ||||
|         APKMirror(), | ||||
|         APKPure(), | ||||
|         Aptoide(), | ||||
|         Uptodown(), | ||||
|         APKMirror(), | ||||
|         HuaweiAppGallery(), | ||||
|         Jenkins(), | ||||
|         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) | ||||
|         Mullvad(), | ||||
|         Signal(), | ||||
|         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 | ||||
|         TelegramApp(), | ||||
|         SteamMobile(), | ||||
| @@ -554,7 +567,9 @@ class SourceProvider { | ||||
|     } | ||||
|     AppSource? source; | ||||
|     for (var s in sources.where((element) => element.host != null)) { | ||||
|       if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) { | ||||
|       if (RegExp( | ||||
|               '://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?') | ||||
|           .hasMatch(url)) { | ||||
|         source = s; | ||||
|         break; | ||||
|       } | ||||
|   | ||||
							
								
								
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -46,10 +46,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: archive | ||||
|       sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" | ||||
|       sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.3.7" | ||||
|     version: "3.3.8" | ||||
|   args: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -206,10 +206,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: easy_localization | ||||
|       sha256: "30ebf25448ffe169e0bd9bc4b5da94faa8398967a2ad2ca09f438be8b6953645" | ||||
|       sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|     version: "3.0.3" | ||||
|   easy_logger: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -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: | ||||
| @@ -783,18 +783,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: url_launcher | ||||
|       sha256: "38d8e783681bc342e92dc9799cbe4421d08c4d210a67ee9d61d0f7310491a465" | ||||
|       sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.1.13" | ||||
|     version: "6.1.14" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: ef7e34951ffa963fb7a65928deeb38d40fb3c5975baf93c1d631341ff7f2650a | ||||
|       sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.0.39" | ||||
|     version: "6.1.0" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -823,10 +823,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_platform_interface | ||||
|       sha256: "4d0dae953f80dc06fa24c58d0f8381302139c22c2dad301417787ad96f5f73bd" | ||||
|       sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.4" | ||||
|     version: "2.1.5" | ||||
|   url_launcher_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -879,18 +879,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webview_flutter_android | ||||
|       sha256: "8545719bbf06f5c4b850f0d5f86da1fe837b1953c56b9af579a26be73627c98d" | ||||
|       sha256: "9427774649fd3c8b7ff53523051395d13aed2ca355822b822e6493d79f5fc05a" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.9.4" | ||||
|     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.6+198 # When changing this, update the tag in main() accordingly | ||||
| version: 0.14.11+203 # 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