mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 21:13:28 +01:00 
			
		
		
		
	Compare commits
	
		
			59 Commits
		
	
	
		
			v0.14.10-b
			...
			v0.14.16-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 734a548bc3 | ||
|  | 591973d97f | ||
|  | a2f57ecd66 | ||
|  | 13dcfb479d | ||
|  | 3328d80130 | ||
|  | f9df3ac0e2 | ||
|  | 61d66e80d8 | ||
|  | 93e0360116 | ||
|  | 2cec5f7934 | ||
|  | 8d4a09b007 | ||
|  | 6d8d2fa99d | ||
|  | 59ecd55530 | ||
|  | 77fcc89e37 | ||
|  | 7d63da4d9c | ||
|  | 8eea0bae99 | ||
|  | f5dcd8f776 | ||
|  | e1f2baeeda | ||
|  | 84d8ff8b0b | ||
|  | 9862c22fdb | ||
|  | 8c82f53dd2 | ||
|  | 5cf4c21ce3 | ||
|  | 4951f62d4a | ||
|  | 4c0c4b7010 | ||
|  | 96051e614c | ||
|  | a2e494b2ba | ||
|  | 2d5a9bec84 | ||
|  | 7d9571cfdd | ||
|  | acc6a780fa | ||
|  | 0e36d42a06 | ||
|  | 5cfddd807a | ||
|  | 17b5604f2a | ||
|  | fdb6eed6d0 | ||
|  | 13de0437b8 | ||
|  | a43c45f310 | ||
|  | 9c56a4d1fc | ||
|  | 2aea1d2631 | ||
|  | 118e05a0fa | ||
|  | 05f497787e | ||
|  | 53cf4d0234 | ||
|  | 6e735b1763 | ||
|  | 873a1a0683 | ||
|  | 27b1149d1c | ||
|  | c1e64f111e | ||
|  | b2af8448fd | ||
|  | 8f44338e76 | ||
|  | e4a55abcb3 | ||
|  | d7348b4973 | ||
|  | 09421230f2 | ||
|  | 4596e32258 | ||
|  | 4dc007a4f6 | ||
|  | c53a156969 | ||
|  | 94bd0774fb | ||
|  | b178b1d780 | ||
|  | cbc840378c | ||
|  | aa7989c16d | ||
|  | 85f9336804 | ||
|  | d66be3ecda | ||
|  | c08e05bd6c | ||
|  | e08ab89fd4 | 
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -2,31 +2,31 @@ | |||||||
| name: Bug report | name: Bug report | ||||||
| about: Something isn't working right. | about: Something isn't working right. | ||||||
| title: '' | title: '' | ||||||
| labels: bug, To Check | labels: bug, to check | ||||||
| assignees: '' | assignees: '' | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| **Prerequisites** | **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** | **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** | **To Reproduce** | ||||||
| Steps to reproduce the behavior: | <!-- Steps to reproduce the behavior: | ||||||
| 1. Go to '...' | 1. Go to '...' | ||||||
| 2. Tap on '....' | 2. Tap on '....' | ||||||
| 3. Scroll down to '....' | 3. Scroll down to '....' | ||||||
| 4. See error | 4. See error --> | ||||||
|  |  | ||||||
| **Screenshots and Logs** | **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:** | **Please complete the following information:** | ||||||
|  - Device: [e.g. Pixel 7] |  - Device: <!-- [e.g. Pixel 7] --> | ||||||
|  - OS: [e.g. GrapheneOS] |  - OS: <!-- [e.g. GrapheneOS] --> | ||||||
|  - Obtainium Version [e.g. 0.14.6-beta] |  - Obtainium Version: <!-- [e.g. 0.14.6-beta] --> | ||||||
|  |  | ||||||
| **Additional context** | **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 | name: Feature request | ||||||
| about: Suggest a new Source, setting, or other feature. | about: Suggest a new Source, setting, or other feature. | ||||||
| title: '' | title: '' | ||||||
| labels: enhancement, To Check | labels: enhancement, to check | ||||||
| assignees: '' | assignees: '' | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| **Prerequisites** | **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** | **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): | 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 | - 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 | - One or more APK URL(s) for the latest release | ||||||
| - Above details for previous releases (optional) | - 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)** | **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** | **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. | Get Android App Updates Directly From the Source. | ||||||
|  |  | ||||||
| Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available. | Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available. | ||||||
|  |  | ||||||
| Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) | ||||||
|  |  | ||||||
| Currently supported App sources: | Currently supported App sources: | ||||||
| - [GitHub](https://github.com/) | - Open Source - General: | ||||||
| - [GitLab](https://gitlab.com/) |   - [GitHub](https://github.com/) | ||||||
| - [Codeberg](https://codeberg.org/) |   - [GitLab](https://gitlab.com/) | ||||||
| - [F-Droid](https://f-droid.org/) |   - [Codeberg](https://codeberg.org/) | ||||||
| - [IzzyOnDroid](https://android.izzysoft.de/) |   - [F-Droid](https://f-droid.org/) | ||||||
| - [Mullvad](https://mullvad.net/en/) |   - Third Party F-Droid Repos | ||||||
| - [Signal](https://signal.org/) |   - [IzzyOnDroid](https://android.izzysoft.de/) | ||||||
| - [SourceForge](https://sourceforge.net/) |   - [SourceForge](https://sourceforge.net/) | ||||||
| - [SourceHut](https://git.sr.ht/) |   - [SourceHut](https://git.sr.ht/) | ||||||
| - [Aptoide](https://aptoide.com/) | - Other - General: | ||||||
| - [APKMirror](https://apkmirror.com/) (Track-Only) |   - [APKPure](https://apkpure.com/) | ||||||
| - [APKPure](https://apkpure.com/) |   - [Aptoide](https://aptoide.com/) | ||||||
| - [Huawei AppGallery](https://appgallery.huawei.com/) |   - [Uptodown](https://uptodown.com/) | ||||||
| - Third Party F-Droid Repos |   - [APKMirror](https://apkmirror.com/) (Track-Only) | ||||||
| - Jenkins Jobs |   - [Huawei AppGallery](https://appgallery.huawei.com/) | ||||||
| - [Steam](https://store.steampowered.com/mobile) |   - Jenkins Jobs | ||||||
| - [Telegram App](https://telegram.org) | - Open Source - App-Specific: | ||||||
| - [Neutron Code](https://neutroncode.com) |   - [Mullvad](https://mullvad.net/en/) | ||||||
| - "HTML" (Fallback) |   - [Signal](https://signal.org/) | ||||||
|   - Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked) |   - [VLC](https://videolan.org/) | ||||||
|  | - Other - App-Specific: | ||||||
|  |   - [Telegram App](https://telegram.org) | ||||||
|  |   - [Steam Mobile Apps](https://store.steampowered.com/mobile) | ||||||
|  |   - [Neutron Code](https://neutroncode.com) | ||||||
|  | - "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Ok", |     "ok": "Ok", | ||||||
|     "and": "e", |     "and": "e", | ||||||
|     "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", |     "githubPATLabel": "Token de Acceso Pessoal do GitHub (Reduz tempos de espera)", | ||||||
|     "githubPATHint": "O TAP deve estar nesse formato: usuario:token", |  | ||||||
|     "githubPATFormat": "usuario:token", |  | ||||||
|     "includePrereleases": "Incluir pré-lançamentos", |     "includePrereleases": "Incluir pré-lançamentos", | ||||||
|     "fallbackToOlderReleases": "Retornar para versões anteriores", |     "fallbackToOlderReleases": "Retornar para versões anteriores", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", |     "filterReleaseTitlesByRegEx": "Filtrar Titulos de Versões por Expressão Regular", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", |     "searchSomeSourcesLabel": "Procurar (Apenas Algumas Fontes)", | ||||||
|     "search": "Procurar", |     "search": "Procurar", | ||||||
|     "additionalOptsFor": "Opções Adicionais para {}", |     "additionalOptsFor": "Opções Adicionais para {}", | ||||||
|     "supportedSourcesBelow": "Fontes Compatíveis:", |     "supportedSources": "Fontes Compatíveis", | ||||||
|     "trackOnlyInBrackets": "(Apenas Seguir)", |     "trackOnlyInBrackets": "(Apenas Seguir)", | ||||||
|     "searchableInBrackets": "(Pesquisável)", |     "searchableInBrackets": "(Pesquisável)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -253,6 +251,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", |     "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", |     "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remover App?", |         "one": "Remover App?", | ||||||
|         "other": "Remover Apps?" |         "other": "Remover Apps?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|    "ok": "Dobro", |    "ok": "Dobro", | ||||||
|    "and": "i", |    "and": "i", | ||||||
|    "githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)", |    "githubPATLabel": "GitHub token za lični pristup (eng. PAT, povećava ograničenje stope)", | ||||||
|    "githubPATHint": "PAT mora biti u ovom formatu: korisničko_ime:token", |  | ||||||
|    "githubPATFormat": "korisničko_ime:token", |  | ||||||
|    "includePrereleases": "Uključi preliminarna izdanja", |    "includePrereleases": "Uključi preliminarna izdanja", | ||||||
|    "fallbackToOlderReleases": "Povratak na starija izdanja", |    "fallbackToOlderReleases": "Povratak na starija izdanja", | ||||||
|    "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", |    "filterReleaseTitlesByRegEx": "Filtrirajte naslove izdanja prema regularnom izrazu", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|    "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", |    "searchSomeSourcesLabel": "Pretraživanje (samo neki izvori)", | ||||||
|    "search": "Pretraživanje", |    "search": "Pretraživanje", | ||||||
|    "additionalOptsFor": "Dodatne opcije za {}", |    "additionalOptsFor": "Dodatne opcije za {}", | ||||||
|    "supportedSourcesBelow": "Podržani izvori:", |    "supportedSources": "Podržani izvori", | ||||||
|    "trackOnlyInBrackets": "(Samo za praćenje)", |    "trackOnlyInBrackets": "(Samo za praćenje)", | ||||||
|    "searchableInBrackets": "(Može se pretraživati)", |    "searchableInBrackets": "(Može se pretraživati)", | ||||||
|    "appsString": "Aplikacije", |    "appsString": "Aplikacije", | ||||||
| @@ -113,7 +111,7 @@ | |||||||
|    "dark": "Tamna", |    "dark": "Tamna", | ||||||
|    "light": "Svijetla", |    "light": "Svijetla", | ||||||
|    "followSystem": "Pratite sistem", |    "followSystem": "Pratite sistem", | ||||||
|    "obtainium": "Obtainium", |     "obtainium": "Obtainium", | ||||||
|    "materialYou": "Material You", |    "materialYou": "Material You", | ||||||
|    "useBlackTheme": "Koristite čisto crnu tamnu temu", |    "useBlackTheme": "Koristite čisto crnu tamnu temu", | ||||||
|    "appSortBy": "Aplikacije sortirane po", |    "appSortBy": "Aplikacije sortirane po", | ||||||
| @@ -250,7 +248,14 @@ | |||||||
|    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|    "removeAppQuestion": { |    "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |    "matchGroupToUse": "Match Group to Use", | ||||||
|  |    "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |    "pickExportDir": "Pick Export Directory", | ||||||
|  |    "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |    "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|  |      "removeAppQuestion": { | ||||||
|       "one": "Želite li ukloniti aplikaciju?", |       "one": "Želite li ukloniti aplikaciju?", | ||||||
|       "other": "Želite li ukloniti aplikacije?" |       "other": "Želite li ukloniti aplikacije?" | ||||||
|    }, |    }, | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "und", |     "and": "und", | ||||||
|     "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", |     "githubPATLabel": "GitHub Personal Access Token (Erhöht das Ratenlimit)", | ||||||
|     "githubPATHint": "PAT muss in diesem Format sein: Benutzername:Token", |  | ||||||
|     "githubPATFormat": "Benutzername:Token", |  | ||||||
|     "includePrereleases": "Vorabversionen einbeziehen", |     "includePrereleases": "Vorabversionen einbeziehen", | ||||||
|     "fallbackToOlderReleases": "Fallback auf ältere Versionen", |     "fallbackToOlderReleases": "Fallback auf ältere Versionen", | ||||||
|     "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", |     "filterReleaseTitlesByRegEx": "Release-Titel nach regulärem Ausdruck\nfiltern", | ||||||
| @@ -30,8 +28,8 @@ | |||||||
|     "xIsTrackOnly": "{} ist nur zur Nachverfolgung", |     "xIsTrackOnly": "{} ist nur zur Nachverfolgung", | ||||||
|     "source": "Quelle", |     "source": "Quelle", | ||||||
|     "app": "App", |     "app": "App", | ||||||
|     "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind 'Nur Nachverfolgen'.", |     "appsFromSourceAreTrackOnly": "Apps aus dieser Quelle sind nur zum Nachverfolgen.", | ||||||
|     "youPickedTrackOnly": "Sie haben die Option 'Nur Nachverfolgen' gewählt.", |     "youPickedTrackOnly": "Sie haben die Option „Nur Nachverfolgen“ gewählt.", | ||||||
|     "trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.", |     "trackOnlyAppDescription": "Die App wird auf Updates überwacht, aber Obtainium wird sie nicht herunterladen oder installieren.", | ||||||
|     "cancelled": "Abgebrochen", |     "cancelled": "Abgebrochen", | ||||||
|     "appAlreadyAdded": "App bereits hinzugefügt", |     "appAlreadyAdded": "App bereits hinzugefügt", | ||||||
| @@ -40,10 +38,10 @@ | |||||||
|     "appSourceURL": "Quell-URL der App", |     "appSourceURL": "Quell-URL der App", | ||||||
|     "error": "Fehler", |     "error": "Fehler", | ||||||
|     "add": "Hinzufügen", |     "add": "Hinzufügen", | ||||||
|     "searchSomeSourcesLabel": "Suche (nur bestimmte Quellen)", |     "searchSomeSourcesLabel": "Suche (nur für bestimmte Quellen)", | ||||||
|     "search": "Suchen", |     "search": "Suchen", | ||||||
|     "additionalOptsFor": "Zusatzoptionen für {}", |     "additionalOptsFor": "Zusatzoptionen für {}", | ||||||
|     "supportedSourcesBelow": "Unterstützte Quellen:", |     "supportedSources": "Unterstützte Quellen", | ||||||
|     "trackOnlyInBrackets": "(Nur Nachverfolgen)", |     "trackOnlyInBrackets": "(Nur Nachverfolgen)", | ||||||
|     "searchableInBrackets": "(Durchsuchbar)", |     "searchableInBrackets": "(Durchsuchbar)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -72,7 +70,7 @@ | |||||||
|     "yes": "Ja", |     "yes": "Ja", | ||||||
|     "markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell", |     "markSelectedAppsUpdated": "Markiere ausgewählte Apps als aktuell", | ||||||
|     "pinToTop": "Oben anheften", |     "pinToTop": "Oben anheften", | ||||||
|     "unpinFromTop": "'Oben anheften' aufheben", |     "unpinFromTop": "„Oben anheften“ aufheben", | ||||||
|     "resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?", |     "resetInstallStatusForSelectedAppsQuestion": "Installationsstatus für ausgewählte Apps zurücksetzen?", | ||||||
|     "installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.", |     "installStatusOfXWillBeResetExplanation": "Der Installationsstatus der ausgewählten Apps wird zurückgesetzt. Dies kann hilfreich sein, wenn die in Obtainium angezeigte App-Version aufgrund fehlgeschlagener Aktualisierungen oder anderer Probleme falsch ist.", | ||||||
|     "shareSelectedAppURLs": "Ausgewählte App-URLs teilen", |     "shareSelectedAppURLs": "Ausgewählte App-URLs teilen", | ||||||
| @@ -177,7 +175,7 @@ | |||||||
|     "appId": "App ID", |     "appId": "App ID", | ||||||
|     "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", |     "appWithIdOrNameNotFound": "Es wurde keine App mit dieser ID oder diesem Namen gefunden", | ||||||
|     "reposHaveMultipleApps": "Repos können mehrere Apps enthalten", |     "reposHaveMultipleApps": "Repos können mehrere Apps enthalten", | ||||||
|     "fdroidThirdPartyRepo": "F-Droid Third-Party Repo", |     "fdroidThirdPartyRepo": "F-Droid Drittparteienrepo", | ||||||
|     "steam": "Steam", |     "steam": "Steam", | ||||||
|     "steamMobile": "Steam Mobile", |     "steamMobile": "Steam Mobile", | ||||||
|     "steamChat": "Steam Chat", |     "steamChat": "Steam Chat", | ||||||
| @@ -246,10 +244,17 @@ | |||||||
|     "xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.", |     "xWasPossiblyUpdatedToY": "{} wurde möglicherweise aktualisiert auf {}.", | ||||||
|     "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.", |     "backgroundUpdateReqsExplanation": "Die Hintergrundaktualisierung ist möglicherweise nicht für alle Apps möglich.", | ||||||
|     "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.", |     "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.", | ||||||
|     "verifyLatestTag": "Überprüfe das 'latest' Tag", |     "verifyLatestTag": "Überprüfe das „latest“ Tag", | ||||||
|     "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", |     "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", |     "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Automatisch höchste APK-Code-Version auswählen", | ||||||
|  |     "versionExtractionRegEx": "Versions-Extraktion RegEx", | ||||||
|  |     "matchGroupToUse": "Zu verwendende Gruppe abgleichen", | ||||||
|  |     "highlightTouchTargets": "Weniger offensichtliche Ziele hervorheben", | ||||||
|  |     "pickExportDir": "Export-Verzeichnis wählen", | ||||||
|  |     "autoExportOnChanges": "Automatischer Export bei Änderung", | ||||||
|  |     "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Versuchen, die vorgeschlagene APK-Code-Version auszuwählen", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "App entfernen?", |         "one": "App entfernen?", | ||||||
|         "other": "Apps entfernen?" |         "other": "Apps entfernen?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "and", |     "and": "and", | ||||||
|     "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", |     "githubPATLabel": "GitHub Personal Access Token (Increases Rate Limit)", | ||||||
|     "githubPATHint": "PAT must be in this format: username:token", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "Include prereleases", |     "includePrereleases": "Include prereleases", | ||||||
|     "fallbackToOlderReleases": "Fallback to older releases", |     "fallbackToOlderReleases": "Fallback to older releases", | ||||||
|     "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", |     "filterReleaseTitlesByRegEx": "Filter Release Titles by Regular Expression", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Search (Some Sources Only)", |     "searchSomeSourcesLabel": "Search (Some Sources Only)", | ||||||
|     "search": "Search", |     "search": "Search", | ||||||
|     "additionalOptsFor": "Additional Options for {}", |     "additionalOptsFor": "Additional Options for {}", | ||||||
|     "supportedSourcesBelow": "Supported Sources:", |     "supportedSources": "Supported Sources", | ||||||
|     "trackOnlyInBrackets": "(Track-Only)", |     "trackOnlyInBrackets": "(Track-Only)", | ||||||
|     "searchableInBrackets": "(Searchable)", |     "searchableInBrackets": "(Searchable)", | ||||||
|     "appsString": "Apps", |     "appsString": "Apps", | ||||||
| @@ -253,6 +251,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Remove App?", |         "one": "Remove App?", | ||||||
|         "other": "Remove Apps?" |         "other": "Remove Apps?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Correcto", |     "ok": "Correcto", | ||||||
|     "and": "y", |     "and": "y", | ||||||
|     "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", |     "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", | ||||||
|     "githubPATHint": "El TAP debe tener este formato: nombre_de_usuario:token", |  | ||||||
|     "githubPATFormat": "nombre_de_usuario:token", |  | ||||||
|     "includePrereleases": "Incluir versiones preliminares", |     "includePrereleases": "Incluir versiones preliminares", | ||||||
|     "fallbackToOlderReleases": "Retorceder a versiones previas", |     "fallbackToOlderReleases": "Retorceder a versiones previas", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", |     "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", |     "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", | ||||||
|     "search": "Buscar", |     "search": "Buscar", | ||||||
|     "additionalOptsFor": "Opciones Adicionales para {}", |     "additionalOptsFor": "Opciones Adicionales para {}", | ||||||
|     "supportedSourcesBelow": "Fuentes Soportadas:", |     "supportedSources": "Fuentes Soportadas", | ||||||
|     "trackOnlyInBrackets": "(Solo Seguimiento)", |     "trackOnlyInBrackets": "(Solo Seguimiento)", | ||||||
|     "searchableInBrackets": "(Soporta Búsquedas)", |     "searchableInBrackets": "(Soporta Búsquedas)", | ||||||
|     "appsString": "Aplicaciones", |     "appsString": "Aplicaciones", | ||||||
| @@ -250,6 +248,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "¿Eliminar Aplicación?", |         "one": "¿Eliminar Aplicación?", | ||||||
|         "other": "¿Eliminar Aplicaciones?" |         "other": "¿Eliminar Aplicaciones?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "باشه", |     "ok": "باشه", | ||||||
|     "and": "و", |     "and": "و", | ||||||
|     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", |     "githubPATLabel": "توکن دسترسی شخصی گیت هاب(محدودیت نرخ را افزایش میدهد)", | ||||||
|     "githubPATHint": "PAT باید در این قالب باشد: username:token", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "شامل نسخه های اولیه", |     "includePrereleases": "شامل نسخه های اولیه", | ||||||
|     "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", |     "fallbackToOlderReleases": "بازگشت به نسخه های قدیمی تر", | ||||||
|     "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", |     "filterReleaseTitlesByRegEx": "عناوین انتشار را با بیان منظم فیلتر کنید", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", |     "searchSomeSourcesLabel": "جستجو (فقط برخی منابع)", | ||||||
|     "search": "جستجو کردن", |     "search": "جستجو کردن", | ||||||
|     "additionalOptsFor": "گزینه های اضافی برای {}", |     "additionalOptsFor": "گزینه های اضافی برای {}", | ||||||
|     "supportedSourcesBelow": "منابع پشتیبانی شده:", |     "supportedSources": "منابع پشتیبانی شده", | ||||||
|     "trackOnlyInBrackets": "«فقط ردیابی»", |     "trackOnlyInBrackets": "«فقط ردیابی»", | ||||||
|     "searchableInBrackets": "(قابل جستجو)", |     "searchableInBrackets": "(قابل جستجو)", | ||||||
|     "appsString": "برنامه ها", |     "appsString": "برنامه ها", | ||||||
| @@ -250,6 +248,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "برنامه حذف شود؟", |         "one": "برنامه حذف شود؟", | ||||||
|         "other": "برنامه ها حذف شوند؟" |         "other": "برنامه ها حذف شوند؟" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Okay", |     "ok": "Okay", | ||||||
|     "and": "et", |     "and": "et", | ||||||
|     "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", |     "githubPATLabel": "Jeton d'Accès Personnel GitHub (Augmente la limite de débit)", | ||||||
|     "githubPATHint": "Le JAP doit être dans ce format : username:token", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "Inclure les avant-premières", |     "includePrereleases": "Inclure les avant-premières", | ||||||
|     "fallbackToOlderReleases": "Retour aux anciennes versions", |     "fallbackToOlderReleases": "Retour aux anciennes versions", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", |     "filterReleaseTitlesByRegEx": "Filtrer les titres de version par expression régulière", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", |     "searchSomeSourcesLabel": "Rechercher (certaines sources uniquement)", | ||||||
|     "search": "Rechercher", |     "search": "Rechercher", | ||||||
|     "additionalOptsFor": "Options supplémentaires pour {}", |     "additionalOptsFor": "Options supplémentaires pour {}", | ||||||
|     "supportedSourcesBelow": "Sources prises en charge :", |     "supportedSources": "Sources prises en charge ", | ||||||
|     "trackOnlyInBrackets": "(Suivi uniquement)", |     "trackOnlyInBrackets": "(Suivi uniquement)", | ||||||
|     "searchableInBrackets": "(Recherchable)", |     "searchableInBrackets": "(Recherchable)", | ||||||
|     "appsString": "Applications", |     "appsString": "Applications", | ||||||
| @@ -250,6 +248,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Supprimer l'application ?", |         "one": "Supprimer l'application ?", | ||||||
|         "other": "Supprimer les applications ?" |         "other": "Supprimer les applications ?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Oké", |     "ok": "Oké", | ||||||
|     "and": "és", |     "and": "és", | ||||||
|     "githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)", |     "githubPATLabel": "GitHub Personal Access Token (megnöveli a díjkorlátot)", | ||||||
|     "githubPATHint": "A PAT-nak a következő formátumban kell lennie: felhasználónév:token", |  | ||||||
|     "githubPATFormat": "felhasználónév:token", |  | ||||||
|     "includePrereleases": "Tartalmazza az előzetes kiadásokat", |     "includePrereleases": "Tartalmazza az előzetes kiadásokat", | ||||||
|     "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", |     "fallbackToOlderReleases": "Visszatérés a régebbi kiadásokhoz", | ||||||
|     "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", |     "filterReleaseTitlesByRegEx": "A kiadás címeinek szűrése reguláris kifejezéssel", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Keresés (csak egyes források)", |     "searchSomeSourcesLabel": "Keresés (csak egyes források)", | ||||||
|     "search": "Keresés", |     "search": "Keresés", | ||||||
|     "additionalOptsFor": "További lehetőségek a következőhöz: {}", |     "additionalOptsFor": "További lehetőségek a következőhöz: {}", | ||||||
|     "supportedSourcesBelow": "Támogatott források:", |     "supportedSources": "Támogatott források", | ||||||
|     "trackOnlyInBrackets": "(Csak nyomonkövetés)", |     "trackOnlyInBrackets": "(Csak nyomonkövetés)", | ||||||
|     "searchableInBrackets": "(Kereshető)", |     "searchableInBrackets": "(Kereshető)", | ||||||
|     "appsString": "Appok", |     "appsString": "Appok", | ||||||
| @@ -248,7 +246,14 @@ | |||||||
|     "verifyLatestTag": "Ellenőrizze a „legújabb” címkét", |     "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)", |     "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", |     "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása", | ||||||
|  |     "versionExtractionRegEx": "Verzió kibontása reguláris kifejezéssel", | ||||||
|  |     "matchGroupToUse": "Párosítsa a csoportot a használathoz", | ||||||
|  |     "highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat", | ||||||
|  |     "pickExportDir": "Válassza az Exportálási könyvtárat", | ||||||
|  |     "autoExportOnChanges": "Auto-exportálás a változások után", | ||||||
|  |     "filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Eltávolítja az alkalmazást?", |         "one": "Eltávolítja az alkalmazást?", | ||||||
|         "other": "Eltávolítja az alkalmazást?" |         "other": "Eltávolítja az alkalmazást?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Va bene", |     "ok": "Va bene", | ||||||
|     "and": "e", |     "and": "e", | ||||||
|     "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", |     "githubPATLabel": "GitHub Personal Access Token (diminuisce limite di traffico)", | ||||||
|     "githubPATHint": "PAT deve seguire questo formato: nomeutente:token", |  | ||||||
|     "githubPATFormat": "nomeutente:token", |  | ||||||
|     "includePrereleases": "Includi prerelease", |     "includePrereleases": "Includi prerelease", | ||||||
|     "fallbackToOlderReleases": "Ripiega su release precedenti", |     "fallbackToOlderReleases": "Ripiega su release precedenti", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", |     "filterReleaseTitlesByRegEx": "Filtra release con espressioni regolari", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", |     "searchSomeSourcesLabel": "Cerca (solo per alcune fonti)", | ||||||
|     "search": "Cerca", |     "search": "Cerca", | ||||||
|     "additionalOptsFor": "Opzioni aggiuntive per {}", |     "additionalOptsFor": "Opzioni aggiuntive per {}", | ||||||
|     "supportedSourcesBelow": "Fonti supportate:", |     "supportedSources": "Fonti supportate", | ||||||
|     "trackOnlyInBrackets": "(Solo-Monitoraggio)", |     "trackOnlyInBrackets": "(Solo-Monitoraggio)", | ||||||
|     "searchableInBrackets": "(ricercabile)", |     "searchableInBrackets": "(ricercabile)", | ||||||
|     "appsString": "App", |     "appsString": "App", | ||||||
| @@ -250,6 +248,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Rimuovere l'app?", |         "one": "Rimuovere l'app?", | ||||||
|         "other": "Rimuovere le app?" |         "other": "Rimuovere le app?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "OK", |     "ok": "OK", | ||||||
|     "and": "と", |     "and": "と", | ||||||
|     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", |     "githubPATLabel": "GitHub パーソナルアクセストークン (レート制限の引き上げ)", | ||||||
|     "githubPATHint": "PATは次の形式でなければなりません: ユーザー名:トークン", |  | ||||||
|     "githubPATFormat": "ユーザー名:トークン", |  | ||||||
|     "includePrereleases": "プレリリースを含む", |     "includePrereleases": "プレリリースを含む", | ||||||
|     "fallbackToOlderReleases": "旧リリースへのフォールバック", |     "fallbackToOlderReleases": "旧リリースへのフォールバック", | ||||||
|     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", |     "filterReleaseTitlesByRegEx": "正規表現でリリースタイトルをフィルタリングする", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "検索 (一部ソースのみ)", |     "searchSomeSourcesLabel": "検索 (一部ソースのみ)", | ||||||
|     "search": "検索", |     "search": "検索", | ||||||
|     "additionalOptsFor": "{}の追加オプション", |     "additionalOptsFor": "{}の追加オプション", | ||||||
|     "supportedSourcesBelow": "対応するソース:", |     "supportedSources": "対応するソース", | ||||||
|     "trackOnlyInBrackets": "(追跡のみ)", |     "trackOnlyInBrackets": "(追跡のみ)", | ||||||
|     "searchableInBrackets": "(検索可能)", |     "searchableInBrackets": "(検索可能)", | ||||||
|     "appsString": "アプリ", |     "appsString": "アプリ", | ||||||
| @@ -146,7 +144,7 @@ | |||||||
|     "updatesAvailable": "アップデートが利用可能", |     "updatesAvailable": "アップデートが利用可能", | ||||||
|     "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", |     "updatesAvailableNotifDescription": "Obtainiumが追跡している1つまたは複数のアプリのアップデートが利用可能であることをユーザーに通知する", | ||||||
|     "noNewUpdates": "新しいアップデートはありません", |     "noNewUpdates": "新しいアップデートはありません", | ||||||
|     "xHasAnUpdate": "{} のアップデートが利用可能です", |     "xHasAnUpdate": "{} のアップデートが利用可能です。", | ||||||
|     "appsUpdated": "アプリをアップデートしました", |     "appsUpdated": "アプリをアップデートしました", | ||||||
|     "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", |     "appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する", | ||||||
|     "xWasUpdatedToY": "{} が {} にアップデートされました", |     "xWasUpdatedToY": "{} が {} にアップデートされました", | ||||||
| @@ -243,14 +241,23 @@ | |||||||
|     "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')", |     "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')", | ||||||
|     "appsPossiblyUpdated": "アプリのアップデートを試行", |     "appsPossiblyUpdated": "アプリのアップデートを試行", | ||||||
|     "appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する", |     "appsPossiblyUpdatedNotifDescription": "1つまたは複数のアプリのアップデートがバックグラウンドで適用された可能性があることをユーザーに通知する", | ||||||
|     "xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります", |     "xWasPossiblyUpdatedToY": "{} が {} にアップデートされた可能性があります。", | ||||||
|     "enableBackgroundUpdates": "バックグラウンドアップデートを有効化する", |     "enableBackgroundUpdates": "バックグラウンドアップデートを有効化する", | ||||||
|     "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。", |     "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。", | ||||||
|     "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。", |     "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。", | ||||||
|     "verifyLatestTag": "'latest'タグを確認する", |     "verifyLatestTag": "'latest'タグを確認する", | ||||||
|  |     "intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする", | ||||||
|  |     "intermediateLinkNotFound": "中間リンクが見つかりませんでした", | ||||||
|     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", |     "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)", | ||||||
|     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", |     "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する", | ||||||
|  |     "versionExtractionRegEx": "バージョン抽出の正規表現", | ||||||
|  |     "matchGroupToUse": "使用するマッチしたグループ", | ||||||
|  |     "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする", | ||||||
|  |     "pickExportDir": "エクスポートディレクトリを選択", | ||||||
|  |     "autoExportOnChanges": "変更があった際に自動でエクスポートする", | ||||||
|  |     "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする", | ||||||
|  |     "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "アプリを削除しますか?", |         "one": "アプリを削除しますか?", | ||||||
|         "other": "アプリを削除しますか?" |         "other": "アプリを削除しますか?" | ||||||
| @@ -292,15 +299,15 @@ | |||||||
|         "other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})" |         "other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})" | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesAvailable": { |     "xAndNMoreUpdatesAvailable": { | ||||||
|         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です", |         "one": "{} とさらに {} 個のアプリのアップデートが利用可能です。", | ||||||
|         "other": "{} とさらに {} 個のアプリのアップデートが利用可能です" |         "other": "{} とさらに {} 個のアプリのアップデートが利用可能です。" | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesInstalled": { |     "xAndNMoreUpdatesInstalled": { | ||||||
|         "one": "{} とさらに {} 個のアプリがアップデートされました", |         "one": "{} とさらに {} 個のアプリがアップデートされました。", | ||||||
|         "other": "{} とさらに {} 個のアプリがアップデートされました" |         "other": "{} とさらに {} 個のアプリがアップデートされました。" | ||||||
|     }, |     }, | ||||||
|     "xAndNMoreUpdatesPossiblyInstalled": { |     "xAndNMoreUpdatesPossiblyInstalled": { | ||||||
|         "one": "{} とさらに 1 個のアプリがアップデートされた可能性があります", |         "one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。", | ||||||
|         "other": "{} とさらに {} 個のアプリがアップデートされた可能性があります" |         "other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,8 +22,6 @@ | |||||||
|     "ok": "Okej", |     "ok": "Okej", | ||||||
|     "and": "i", |     "and": "i", | ||||||
|     "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)", |     "githubPATLabel": "Osobisty token dostępu GitHub (zwiększa limit zapytań)", | ||||||
|     "githubPATHint": "Wymagany format: użytkownik:token", |  | ||||||
|     "githubPATFormat": "użytkownik:token", |  | ||||||
|     "includePrereleases": "Uwzględnij wersje wstępne", |     "includePrereleases": "Uwzględnij wersje wstępne", | ||||||
|     "fallbackToOlderReleases": "Powracaj do starszych wersji", |     "fallbackToOlderReleases": "Powracaj do starszych wersji", | ||||||
|     "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", |     "filterReleaseTitlesByRegEx": "Filtruj tytuły wydań wg. wyrażeń regularnych", | ||||||
| @@ -52,7 +50,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", |     "searchSomeSourcesLabel": "Szukaj (tylko niektóre źródła)", | ||||||
|     "search": "Szukaj", |     "search": "Szukaj", | ||||||
|     "additionalOptsFor": "Dodatkowe opcje dla {}", |     "additionalOptsFor": "Dodatkowe opcje dla {}", | ||||||
|     "supportedSourcesBelow": "Obsługiwane źródła:", |     "supportedSources": "Obsługiwane źródła", | ||||||
|     "trackOnlyInBrackets": "(tylko obserwowane)", |     "trackOnlyInBrackets": "(tylko obserwowane)", | ||||||
|     "searchableInBrackets": "(Wyszukiwalne)", |     "searchableInBrackets": "(Wyszukiwalne)", | ||||||
|     "appsString": "Aplikacje", |     "appsString": "Aplikacje", | ||||||
| @@ -255,7 +253,14 @@ | |||||||
|     "verifyLatestTag": "Zweryfikuj najnowszy tag", |     "verifyLatestTag": "Zweryfikuj najnowszy tag", | ||||||
|     "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)", |     "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi", |     "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK", | ||||||
|  |     "versionExtractionRegEx": "Wyrażenie regularne wyodrębniające wersję", | ||||||
|  |     "matchGroupToUse": "Dopasuj grupę do użycia", | ||||||
|  |     "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe", | ||||||
|  |     "pickExportDir": "Wybierz katalog eksportu", | ||||||
|  |     "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian", | ||||||
|  |     "filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Usunąć aplikację?", |         "one": "Usunąć aplikację?", | ||||||
|         "few": "Usunąć aplikacje?", |         "few": "Usunąć aplikacje?", | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "Окей", |     "ok": "Окей", | ||||||
|     "and": "и", |     "and": "и", | ||||||
|     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", |     "githubPATLabel": "Персональный токен доступа GitHub (увеличивает лимит запросов)", | ||||||
|     "githubPATHint": "Токен доступа должен быть в формате: имя_пользователя:токен", |  | ||||||
|     "githubPATFormat": "имя_пользователя:токен", |  | ||||||
|     "includePrereleases": "Включить предварительные релизы", |     "includePrereleases": "Включить предварительные релизы", | ||||||
|     "fallbackToOlderReleases": "Откатиться к более старым версиям", |     "fallbackToOlderReleases": "Откатиться к более старым версиям", | ||||||
|     "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", |     "filterReleaseTitlesByRegEx": "Фильтровать заголовки релизов\nс помощью регулярного выражения", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", |     "searchSomeSourcesLabel": "Поиск (только в некоторых источниках)", | ||||||
|     "search": "Поиск", |     "search": "Поиск", | ||||||
|     "additionalOptsFor": "Дополнительные опции для {}", |     "additionalOptsFor": "Дополнительные опции для {}", | ||||||
|     "supportedSourcesBelow": "Поддерживаемые источники:", |     "supportedSources": "Поддерживаемые источники", | ||||||
|     "trackOnlyInBrackets": "(Только для отслеживания)", |     "trackOnlyInBrackets": "(Только для отслеживания)", | ||||||
|     "searchableInBrackets": "(Поиск)", |     "searchableInBrackets": "(Поиск)", | ||||||
|     "appsString": "Приложения", |     "appsString": "Приложения", | ||||||
| @@ -250,6 +248,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "Удалить приложение?", |         "one": "Удалить приложение?", | ||||||
|         "other": "Удалить приложения?" |         "other": "Удалить приложения?" | ||||||
|   | |||||||
| @@ -12,8 +12,6 @@ | |||||||
|     "ok": "好的", |     "ok": "好的", | ||||||
|     "and": "和", |     "and": "和", | ||||||
|     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", |     "githubPATLabel": "GitHub 个人访问令牌(提升 API 请求限额)", | ||||||
|     "githubPATHint": "个人访问令牌必须为“username:token”的格式", |  | ||||||
|     "githubPATFormat": "username:token", |  | ||||||
|     "includePrereleases": "包含预发行版", |     "includePrereleases": "包含预发行版", | ||||||
|     "fallbackToOlderReleases": "将旧发行版作为备选", |     "fallbackToOlderReleases": "将旧发行版作为备选", | ||||||
|     "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", |     "filterReleaseTitlesByRegEx": "使用正则表达式筛选发行标题", | ||||||
| @@ -43,7 +41,7 @@ | |||||||
|     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", |     "searchSomeSourcesLabel": "搜索(仅支持部分来源)", | ||||||
|     "search": "搜索", |     "search": "搜索", | ||||||
|     "additionalOptsFor": "{} 的更多选项", |     "additionalOptsFor": "{} 的更多选项", | ||||||
|     "supportedSourcesBelow": "支持的来源:", |     "supportedSources": "支持的来源", | ||||||
|     "trackOnlyInBrackets": "(仅追踪)", |     "trackOnlyInBrackets": "(仅追踪)", | ||||||
|     "searchableInBrackets": "(可搜索)", |     "searchableInBrackets": "(可搜索)", | ||||||
|     "appsString": "应用列表", |     "appsString": "应用列表", | ||||||
| @@ -251,6 +249,13 @@ | |||||||
|     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", |     "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", | ||||||
|     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", |     "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", | ||||||
|     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", |     "autoSelectHighestVersionCode": "Auto-select highest versionCode APK", | ||||||
|  |     "versionExtractionRegEx": "Version Extraction RegEx", | ||||||
|  |     "matchGroupToUse": "Match Group to Use", | ||||||
|  |     "highlightTouchTargets": "Highlight less obvious touch targets", | ||||||
|  |     "pickExportDir": "Pick Export Directory", | ||||||
|  |     "autoExportOnChanges": "Auto-export on changes", | ||||||
|  |     "filterVersionsByRegEx": "Filter Versions by Regular Expression", | ||||||
|  |     "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", | ||||||
|     "removeAppQuestion": { |     "removeAppQuestion": { | ||||||
|         "one": "是否删除应用?", |         "one": "是否删除应用?", | ||||||
|         "other": "是否删除应用?" |         "other": "是否删除应用?" | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								build.sh
									
									
									
									
									
								
							| @@ -4,7 +4,9 @@ | |||||||
| CURR_DIR="$(pwd)" | CURR_DIR="$(pwd)" | ||||||
| trap "cd "$CURR_DIR"" EXIT | trap "cd "$CURR_DIR"" EXIT | ||||||
|  |  | ||||||
| git fetch && git merge origin/main && git push                                               # Typically run after a PR to main, so bring dev up to date | if [ -z "$1" ]; then | ||||||
|  |     git fetch && git merge origin/main && git push # Typically run after a PR to main, so bring dev up to date | ||||||
|  | fi | ||||||
| rm ./build/app/outputs/flutter-apk/* 2>/dev/null                                             # Get rid of older builds if any | rm ./build/app/outputs/flutter-apk/* 2>/dev/null                                             # Get rid of older builds if any | ||||||
| flutter build apk && flutter build apk --split-per-abi                                       # Build (both split and combined APKs) | flutter build apk && flutter build apk --split-per-abi                                       # Build (both split and combined APKs) | ||||||
| for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures | for file in ./build/app/outputs/flutter-apk/*.sha1; do gpg --sign --detach-sig "$file"; done # Generate PGP signatures | ||||||
|   | |||||||
| @@ -25,12 +25,16 @@ class APKCombo extends AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Map<String, String> get requestHeaders => { |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|         "User-Agent": "curl/8.0.1", |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|         "Accept": "*/*", |       bool forAPKDownload = false}) async { | ||||||
|         "Connection": "keep-alive", |     return { | ||||||
|         "Host": "$host" |       "User-Agent": "curl/8.0.1", | ||||||
|       }; |       "Accept": "*/*", | ||||||
|  |       "Connection": "keep-alive", | ||||||
|  |       "Host": "$host" | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { |   Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async { | ||||||
|     var res = await sourceRequest('$standardUrl/download/apk'); |     var res = await sourceRequest('$standardUrl/download/apk'); | ||||||
|   | |||||||
| @@ -3,6 +3,21 @@ import 'package:html/parser.dart'; | |||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | parseDateTimeMMMddCommayyyy(String? dateString) { | ||||||
|  |   DateTime? releaseDate; | ||||||
|  |   try { | ||||||
|  |     releaseDate = dateString != null | ||||||
|  |         ? DateFormat('MMM dd, yyyy').parse(dateString) | ||||||
|  |         : null; | ||||||
|  |     releaseDate = dateString != null && releaseDate == null | ||||||
|  |         ? DateFormat('MMMM dd, yyyy').parse(dateString) | ||||||
|  |         : releaseDate; | ||||||
|  |   } catch (err) { | ||||||
|  |     // ignore | ||||||
|  |   } | ||||||
|  |   return releaseDate; | ||||||
|  | } | ||||||
|  |  | ||||||
| class APKPure extends AppSource { | class APKPure extends AppSource { | ||||||
|   APKPure() { |   APKPure() { | ||||||
|     host = 'apkpure.com'; |     host = 'apkpure.com'; | ||||||
| @@ -47,17 +62,7 @@ class APKPure extends AppSource { | |||||||
|       } |       } | ||||||
|       String? dateString = |       String? dateString = | ||||||
|           html.querySelector('span.info-other span.date')?.text.trim(); |           html.querySelector('span.info-other span.date')?.text.trim(); | ||||||
|       DateTime? releaseDate; |       DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString); | ||||||
|       try { |  | ||||||
|         releaseDate = dateString != null |  | ||||||
|             ? DateFormat('MMM dd, yyyy').parse(dateString) |  | ||||||
|             : null; |  | ||||||
|         releaseDate = dateString != null && releaseDate == null |  | ||||||
|             ? DateFormat('MMMM dd, yyyy').parse(dateString) |  | ||||||
|             : null; |  | ||||||
|       } catch (err) { |  | ||||||
|         // ignore |  | ||||||
|       } |  | ||||||
|       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; |       String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK'; | ||||||
|       List<MapEntry<String, String>> apkUrls = [ |       List<MapEntry<String, String>> apkUrls = [ | ||||||
|         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') |         MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest') | ||||||
| @@ -70,11 +75,13 @@ class APKPure extends AppSource { | |||||||
|           Uri.parse(standardUrl).pathSegments.reversed.last; |           Uri.parse(standardUrl).pathSegments.reversed.last; | ||||||
|       String appName = |       String appName = | ||||||
|           html.querySelector('h1.info-title')?.text.trim() ?? appId; |           html.querySelector('h1.info-title')?.text.trim() ?? appId; | ||||||
|       String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml |       String? changeLog = htmlChangelog | ||||||
|           .trim().replaceAll("<br>", "  \n"); |           .querySelector("div.whats-new-info p:not(.date)") | ||||||
|  |           ?.innerHtml | ||||||
|  |           .trim() | ||||||
|  |           .replaceAll("<br>", "  \n"); | ||||||
|       return APKDetails(version, apkUrls, AppNames(author, appName), |       return APKDetails(version, apkUrls, AppNames(author, appName), | ||||||
|           releaseDate: releaseDate, |           releaseDate: releaseDate, changeLog: changeLog); | ||||||
|           changeLog: changeLog); |  | ||||||
|     } else { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | 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/custom_errors.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
| @@ -75,34 +73,4 @@ class Aptoide extends AppSource { | |||||||
|         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), |         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||||
|         releaseDate: relDate); |         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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,10 +13,24 @@ class FDroid extends AppSource { | |||||||
|     name = tr('fdroid'); |     name = tr('fdroid'); | ||||||
|     canSearch = true; |     canSearch = true; | ||||||
|     additionalSourceAppSpecificSettingFormItems = [ |     additionalSourceAppSpecificSettingFormItems = [ | ||||||
|  |       [ | ||||||
|  |         GeneratedFormTextField('filterVersionsByRegEx', | ||||||
|  |             label: tr('filterVersionsByRegEx'), | ||||||
|  |             required: false, | ||||||
|  |             additionalValidators: [ | ||||||
|  |               (value) { | ||||||
|  |                 return regExValidator(value); | ||||||
|  |               } | ||||||
|  |             ]) | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         GeneratedFormSwitch('trySelectingSuggestedVersionCode', | ||||||
|  |             label: tr('trySelectingSuggestedVersionCode'), defaultValue: true) | ||||||
|  |       ], | ||||||
|       [ |       [ | ||||||
|         GeneratedFormSwitch('autoSelectHighestVersionCode', |         GeneratedFormSwitch('autoSelectHighestVersionCode', | ||||||
|             label: tr('autoSelectHighestVersionCode')) |             label: tr('autoSelectHighestVersionCode')) | ||||||
|       ] |       ], | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -45,25 +59,73 @@ class FDroid extends AppSource { | |||||||
|  |  | ||||||
|   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( |   APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( | ||||||
|       Response res, String apkUrlPrefix, String standardUrl, |       Response res, String apkUrlPrefix, String standardUrl, | ||||||
|       {bool autoSelectHighestVersionCode = false}) { |       {bool autoSelectHighestVersionCode = false, | ||||||
|  |       bool trySelectingSuggestedVersionCode = false, | ||||||
|  |       String? filterVersionsByRegEx}) { | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       List<dynamic> releases = jsonDecode(res.body)['packages'] ?? []; |       var response = jsonDecode(res.body); | ||||||
|  |       List<dynamic> releases = response['packages'] ?? []; | ||||||
|       if (releases.isEmpty) { |       if (releases.isEmpty) { | ||||||
|         throw NoReleasesError(); |         throw NoReleasesError(); | ||||||
|       } |       } | ||||||
|       String? latestVersion = releases[0]['versionName']; |       String? version; | ||||||
|       if (latestVersion == null) { |       Iterable<dynamic> releaseChoices = []; | ||||||
|  |       // Grab the versionCode suggested if the user chose to do that | ||||||
|  |       // Only do so at this stage if the user has no release filter | ||||||
|  |       if (trySelectingSuggestedVersionCode && | ||||||
|  |           response['suggestedVersionCode'] != null && | ||||||
|  |           filterVersionsByRegEx == null) { | ||||||
|  |         var suggestedReleases = releases.where((element) => | ||||||
|  |             element['versionCode'] == response['suggestedVersionCode']); | ||||||
|  |         if (suggestedReleases.isNotEmpty) { | ||||||
|  |           releaseChoices = suggestedReleases; | ||||||
|  |           version = suggestedReleases.first['versionName']; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // Apply the release filter if any | ||||||
|  |       if (filterVersionsByRegEx != null) { | ||||||
|  |         version = null; | ||||||
|  |         releaseChoices = []; | ||||||
|  |         for (var i = 0; i < releases.length; i++) { | ||||||
|  |           if (RegExp(filterVersionsByRegEx) | ||||||
|  |               .hasMatch(releases[i]['versionName'])) { | ||||||
|  |             version = releases[i]['versionName']; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (version == null) { | ||||||
|  |           throw NoVersionError(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // Default to the highest version | ||||||
|  |       version ??= releases[0]['versionName']; | ||||||
|  |       if (version == null) { | ||||||
|         throw NoVersionError(); |         throw NoVersionError(); | ||||||
|       } |       } | ||||||
|       Iterable<dynamic> latestReleases = |       // If a suggested release was not already picked, pick all those with the selected version | ||||||
|           releases.where((element) => element['versionName'] == latestVersion); |       if (releaseChoices.isEmpty) { | ||||||
|       if (latestReleases.length > 1 && autoSelectHighestVersionCode) { |         releaseChoices = | ||||||
|         latestReleases = [latestReleases.first]; |             releases.where((element) => element['versionName'] == version); | ||||||
|       } |       } | ||||||
|       List<String> apkUrls = latestReleases |       // For the remaining releases, use the toggles to auto-select one if possible | ||||||
|  |       if (releaseChoices.length > 1) { | ||||||
|  |         if (autoSelectHighestVersionCode) { | ||||||
|  |           releaseChoices = [releaseChoices.first]; | ||||||
|  |         } else if (trySelectingSuggestedVersionCode && | ||||||
|  |             response['suggestedVersionCode'] != null) { | ||||||
|  |           var suggestedReleases = releaseChoices.where((element) => | ||||||
|  |               element['versionCode'] == response['suggestedVersionCode']); | ||||||
|  |           if (suggestedReleases.isNotEmpty) { | ||||||
|  |             releaseChoices = suggestedReleases; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (releaseChoices.isEmpty) { | ||||||
|  |         throw NoReleasesError(); | ||||||
|  |       } | ||||||
|  |       List<String> apkUrls = releaseChoices | ||||||
|           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') |           .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk') | ||||||
|           .toList(); |           .toList(); | ||||||
|       return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), |       return APKDetails(version, getApkUrlsFromUrls(apkUrls), | ||||||
|           AppNames(name, Uri.parse(standardUrl).pathSegments.last)); |           AppNames(name, Uri.parse(standardUrl).pathSegments.last)); | ||||||
|     } else { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
| @@ -82,7 +144,15 @@ class FDroid extends AppSource { | |||||||
|         'https://$host/repo/$appId', |         'https://$host/repo/$appId', | ||||||
|         standardUrl, |         standardUrl, | ||||||
|         autoSelectHighestVersionCode: |         autoSelectHighestVersionCode: | ||||||
|             additionalSettings['autoSelectHighestVersionCode'] == true); |             additionalSettings['autoSelectHighestVersionCode'] == true, | ||||||
|  |         trySelectingSuggestedVersionCode: | ||||||
|  |             additionalSettings['trySelectingSuggestedVersionCode'] == true, | ||||||
|  |         filterVersionsByRegEx: | ||||||
|  |             (additionalSettings['filterVersionsByRegEx'] as String?) | ||||||
|  |                         ?.isNotEmpty == | ||||||
|  |                     true | ||||||
|  |                 ? additionalSettings['filterVersionsByRegEx'] | ||||||
|  |                 : null); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
|  | import 'dart:io'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| @@ -21,20 +22,6 @@ class GitHub extends AppSource { | |||||||
|           label: tr('githubPATLabel'), |           label: tr('githubPATLabel'), | ||||||
|           password: true, |           password: true, | ||||||
|           required: false, |           required: false, | ||||||
|           additionalValidators: [ |  | ||||||
|             (value) { |  | ||||||
|               if (value != null && value.trim().isNotEmpty) { |  | ||||||
|                 if (value |  | ||||||
|                         .split(':') |  | ||||||
|                         .where((element) => element.trim().isNotEmpty) |  | ||||||
|                         .length != |  | ||||||
|                     2) { |  | ||||||
|                   return tr('githubPATHint'); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|               return null; |  | ||||||
|             } |  | ||||||
|           ], |  | ||||||
|           hint: tr('githubPATFormat'), |           hint: tr('githubPATFormat'), | ||||||
|           belowWidgets: [ |           belowWidgets: [ | ||||||
|             const SizedBox( |             const SizedBox( | ||||||
| @@ -169,26 +156,53 @@ class GitHub extends AppSource { | |||||||
|     return url.substring(0, match.end); |     return url.substring(0, match.end); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> getCredentialPrefixIfAny( |   @override | ||||||
|       Map<String, dynamic> additionalSettings) async { |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |       bool forAPKDownload = false}) async { | ||||||
|  |     var token = await getTokenIfAny(additionalSettings); | ||||||
|  |     var headers = <String, String>{}; | ||||||
|  |     if (token != null) { | ||||||
|  |       headers[HttpHeaders.authorizationHeader] = 'Token $token'; | ||||||
|  |     } | ||||||
|  |     if (forAPKDownload == true) { | ||||||
|  |       headers[HttpHeaders.acceptHeader] = 'application/octet-stream'; | ||||||
|  |     } | ||||||
|  |     if (headers.isNotEmpty) { | ||||||
|  |       return headers; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<String?> getTokenIfAny(Map<String, dynamic> additionalSettings) async { | ||||||
|     SettingsProvider settingsProvider = SettingsProvider(); |     SettingsProvider settingsProvider = SettingsProvider(); | ||||||
|     await settingsProvider.initializeSettings(); |     await settingsProvider.initializeSettings(); | ||||||
|     var sourceConfig = |     var sourceConfig = | ||||||
|         await getSourceConfigValues(additionalSettings, settingsProvider); |         await getSourceConfigValues(additionalSettings, settingsProvider); | ||||||
|     String? creds = sourceConfig['github-creds']; |     String? creds = sourceConfig['github-creds']; | ||||||
|     return creds != null && creds.isNotEmpty ? '$creds@' : ''; |     if (creds != null) { | ||||||
|  |       var userNameEndIndex = creds.indexOf(':'); | ||||||
|  |       if (userNameEndIndex > 0) { | ||||||
|  |         creds = creds.substring( | ||||||
|  |             userNameEndIndex + 1); // For old username-included token inputs | ||||||
|  |       } | ||||||
|  |       return creds; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<String?> getSourceNote() async { |   Future<String?> getSourceNote() async { | ||||||
|     if (!hostChanged && (await getCredentialPrefixIfAny({})).isEmpty) { |     if (!hostChanged && (await getTokenIfAny({})) == null) { | ||||||
|       return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; |       return '${tr('githubSourceNote')} ${hostChanged ? tr('addInfoBelow') : tr('addInfoInSettings')}'; | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => |   Future<String> getAPIHost(Map<String, dynamic> additionalSettings) async => | ||||||
|       'https://${await getCredentialPrefixIfAny(additionalSettings)}api.$host'; |       'https://api.$host'; | ||||||
|  |  | ||||||
|   Future<String> convertStandardUrlToAPIUrl( |   Future<String> convertStandardUrlToAPIUrl( | ||||||
|           String standardUrl, Map<String, dynamic> additionalSettings) async => |           String standardUrl, Map<String, dynamic> additionalSettings) async => | ||||||
| @@ -238,9 +252,10 @@ class GitHub extends AppSource { | |||||||
|       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => |       List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => | ||||||
|           (release['assets'] as List<dynamic>?) |           (release['assets'] as List<dynamic>?) | ||||||
|               ?.map((e) { |               ?.map((e) { | ||||||
|                 return e['name'] != null && e['browser_download_url'] != null |                 return (e['name'] != null) && | ||||||
|  |                         ((e['url'] ?? e['browser_download_url']) != null) | ||||||
|                     ? MapEntry(e['name'] as String, |                     ? MapEntry(e['name'] as String, | ||||||
|                         e['browser_download_url'] as String) |                         (e['url'] ?? e['browser_download_url']) as String) | ||||||
|                     : const MapEntry('', ''); |                     : const MapEntry('', ''); | ||||||
|               }) |               }) | ||||||
|               .where((element) => element.key.toLowerCase().endsWith('.apk')) |               .where((element) => element.key.toLowerCase().endsWith('.apk')) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
| import 'package:html/parser.dart'; | import 'package:html/parser.dart'; | ||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
| @@ -109,6 +110,23 @@ class HTML extends AppSource { | |||||||
|             hint: '([0-9]+\.)*[0-9]+/\$', |             hint: '([0-9]+\.)*[0-9]+/\$', | ||||||
|             required: false, |             required: false, | ||||||
|             additionalValidators: [(value) => regExValidator(value)]) |             additionalValidators: [(value) => regExValidator(value)]) | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         GeneratedFormTextField('versionExtractionRegEx', | ||||||
|  |             label: tr('versionExtractionRegEx'), | ||||||
|  |             required: false, | ||||||
|  |             additionalValidators: [(value) => regExValidator(value)]), | ||||||
|  |         GeneratedFormTextField('matchGroupToUse', | ||||||
|  |             label: tr('matchGroupToUse'), | ||||||
|  |             required: false, | ||||||
|  |             hint: '1', | ||||||
|  |             textInputType: const TextInputType.numberWithOptions(), | ||||||
|  |             additionalValidators: [ | ||||||
|  |               (value) { | ||||||
|  |                 value ??= '1'; | ||||||
|  |                 return intValidator(value); | ||||||
|  |               } | ||||||
|  |             ]) | ||||||
|       ] |       ] | ||||||
|     ]; |     ]; | ||||||
|     overrideVersionDetectionFormDefault('noVersionDetection', |     overrideVersionDetectionFormDefault('noVersionDetection', | ||||||
| @@ -116,11 +134,14 @@ class HTML extends AppSource { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   // TODO: implement requestHeaders choice, hardcoded for now |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|   Map<String, String>? get requestHeaders => { |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|         "User-Agent": |       bool forAPKDownload = false}) async { | ||||||
|             "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" |     return { | ||||||
|       }; |       "User-Agent": | ||||||
|  |           "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String sourceSpecificStandardizeURL(String url) { |   String sourceSpecificStandardizeURL(String url) { | ||||||
| @@ -180,10 +201,23 @@ class HTML extends AppSource { | |||||||
|         throw NoReleasesError(); |         throw NoReleasesError(); | ||||||
|       } |       } | ||||||
|       var rel = links.last; |       var rel = links.last; | ||||||
|       var version = rel.hashCode.toString(); |       String? version = rel.hashCode.toString(); | ||||||
|  |       var versionExtractionRegEx = | ||||||
|  |           additionalSettings['versionExtractionRegEx'] as String?; | ||||||
|  |       if (versionExtractionRegEx?.isNotEmpty == true) { | ||||||
|  |         var match = RegExp(versionExtractionRegEx!).allMatches(rel); | ||||||
|  |         if (match.isEmpty) { | ||||||
|  |           throw NoVersionError(); | ||||||
|  |         } | ||||||
|  |         version = match.last | ||||||
|  |             .group(int.parse(additionalSettings['matchGroupToUse'] as String)); | ||||||
|  |         if (version?.isEmpty == true) { | ||||||
|  |           throw NoVersionError(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       List<String> apkUrls = |       List<String> apkUrls = | ||||||
|           [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); |           [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); | ||||||
|       return APKDetails(version, apkUrls.map((e) => MapEntry(e, e)).toList(), |       return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(), | ||||||
|           AppNames(uri.host, tr('app'))); |           AppNames(uri.host, tr('app'))); | ||||||
|     } else { |     } else { | ||||||
|       throw getObtainiumHttpError(res); |       throw getObtainiumHttpError(res); | ||||||
|   | |||||||
| @@ -40,6 +40,9 @@ class IzzyOnDroid extends AppSource { | |||||||
|         'https://android.izzysoft.de/frepo/$appId', |         'https://android.izzysoft.de/frepo/$appId', | ||||||
|         standardUrl, |         standardUrl, | ||||||
|         autoSelectHighestVersionCode: |         autoSelectHighestVersionCode: | ||||||
|             additionalSettings['autoSelectHighestVersionCode'] == true); |             additionalSettings['autoSelectHighestVersionCode'] == true, | ||||||
|  |         trySelectingSuggestedVersionCode: | ||||||
|  |             additionalSettings['trySelectingSuggestedVersionCode'] == true, | ||||||
|  |         filterVersionsByRegEx: additionalSettings['filterVersionsByRegEx']); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/app_sources/uptodown.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:html/parser.dart'; | ||||||
|  | import 'package:obtainium/app_sources/apkpure.dart'; | ||||||
|  | import 'package:obtainium/custom_errors.dart'; | ||||||
|  | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  |  | ||||||
|  | class Uptodown extends AppSource { | ||||||
|  |   Uptodown() { | ||||||
|  |     host = 'uptodown.com'; | ||||||
|  |     allowSubDomains = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String sourceSpecificStandardizeURL(String url) { | ||||||
|  |     RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host'); | ||||||
|  |     RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); | ||||||
|  |     if (match == null) { | ||||||
|  |       throw InvalidURLError(name); | ||||||
|  |     } | ||||||
|  |     return '${url.substring(0, match.end)}/android/download'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<String?> tryInferringAppId(String standardUrl, | ||||||
|  |       {Map<String, dynamic> additionalSettings = const {}}) async { | ||||||
|  |     return (await getAppDetailsFromPage(standardUrl))['appId']; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async { | ||||||
|  |     var res = await sourceRequest(standardUrl); | ||||||
|  |     if (res.statusCode != 200) { | ||||||
|  |       throw getObtainiumHttpError(res); | ||||||
|  |     } | ||||||
|  |     var html = parse(res.body); | ||||||
|  |     String? version = html.querySelector('div.version')?.innerHtml; | ||||||
|  |     String? apkUrl = | ||||||
|  |         html.querySelector('#detail-download-button')?.attributes['data-url']; | ||||||
|  |     String? name = html.querySelector('#detail-app-name')?.innerHtml.trim(); | ||||||
|  |     String? author = html.querySelector('#author-link')?.innerHtml.trim(); | ||||||
|  |     var detailElements = html.querySelectorAll('#technical-information td'); | ||||||
|  |     String? appId = (detailElements.elementAtOrNull(2))?.innerHtml.trim(); | ||||||
|  |     String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml.trim(); | ||||||
|  |     return Map.fromEntries([ | ||||||
|  |       MapEntry('version', version), | ||||||
|  |       MapEntry('apkUrl', apkUrl), | ||||||
|  |       MapEntry('appId', appId), | ||||||
|  |       MapEntry('name', name), | ||||||
|  |       MapEntry('author', author), | ||||||
|  |       MapEntry('dateStr', dateStr) | ||||||
|  |     ]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Future<APKDetails> getLatestAPKDetails( | ||||||
|  |     String standardUrl, | ||||||
|  |     Map<String, dynamic> additionalSettings, | ||||||
|  |   ) async { | ||||||
|  |     var appDetails = await getAppDetailsFromPage(standardUrl); | ||||||
|  |     var version = appDetails['version']; | ||||||
|  |     var apkUrl = appDetails['apkUrl']; | ||||||
|  |     var appId = appDetails['appId']; | ||||||
|  |     if (version == null) { | ||||||
|  |       throw NoVersionError(); | ||||||
|  |     } | ||||||
|  |     if (apkUrl == null) { | ||||||
|  |       throw NoAPKError(); | ||||||
|  |     } | ||||||
|  |     if (appId == null) { | ||||||
|  |       throw NoReleasesError(); | ||||||
|  |     } | ||||||
|  |     String appName = appDetails['name'] ?? tr('app'); | ||||||
|  |     String author = appDetails['author'] ?? name; | ||||||
|  |     String? dateStr = appDetails['dateStr']; | ||||||
|  |     DateTime? relDate; | ||||||
|  |     if (dateStr != null) { | ||||||
|  |       relDate = parseDateTimeMMMddCommayyyy(dateStr); | ||||||
|  |     } | ||||||
|  |     return APKDetails( | ||||||
|  |         version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName), | ||||||
|  |         releaseDate: relDate); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -12,7 +12,12 @@ class VLC extends AppSource { | |||||||
|   get dwUrlBase => 'https://get.$host/vlc-android/'; |   get dwUrlBase => 'https://get.$host/vlc-android/'; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Map<String, String>? get requestHeaders => HTML().requestHeaders; |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |           {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |           bool forAPKDownload = false}) => | ||||||
|  |       HTML().getRequestHeaders( | ||||||
|  |           additionalSettings: additionalSettings, | ||||||
|  |           forAPKDownload: forAPKDownload); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String sourceSpecificStandardizeURL(String url) { |   String sourceSpecificStandardizeURL(String url) { | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ class GeneratedFormTextField extends GeneratedFormItem { | |||||||
|   late int max; |   late int max; | ||||||
|   late String? hint; |   late String? hint; | ||||||
|   late bool password; |   late bool password; | ||||||
|  |   late TextInputType? textInputType; | ||||||
|  |  | ||||||
|   GeneratedFormTextField(String key, |   GeneratedFormTextField(String key, | ||||||
|       {String label = 'Input', |       {String label = 'Input', | ||||||
| @@ -34,7 +35,8 @@ class GeneratedFormTextField extends GeneratedFormItem { | |||||||
|       this.required = true, |       this.required = true, | ||||||
|       this.max = 1, |       this.max = 1, | ||||||
|       this.hint, |       this.hint, | ||||||
|       this.password = false}) |       this.password = false, | ||||||
|  |       this.textInputType}) | ||||||
|       : super(key, |       : super(key, | ||||||
|             label: label, |             label: label, | ||||||
|             belowWidgets: belowWidgets, |             belowWidgets: belowWidgets, | ||||||
| @@ -144,7 +146,8 @@ Color generateRandomLightColor() { | |||||||
|   // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 |   // Map from HPLuv color space to RGB, use constant saturation=100, lightness=70 | ||||||
|   final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); |   final List<double> rgbValuesDbl = Hsluv.hpluvToRgb([hue, 100, 70]); | ||||||
|   // Map RBG values from 0-1 to 0-255: |   // Map RBG values from 0-1 to 0-255: | ||||||
|   final List<int> rgbValues = rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); |   final List<int> rgbValues = | ||||||
|  |       rgbValuesDbl.map((rgb) => (rgb * 255).toInt()).toList(); | ||||||
|   return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); |   return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -190,6 +193,7 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|         if (formItem is GeneratedFormTextField) { |         if (formItem is GeneratedFormTextField) { | ||||||
|           final formFieldKey = GlobalKey<FormFieldState>(); |           final formFieldKey = GlobalKey<FormFieldState>(); | ||||||
|           return TextFormField( |           return TextFormField( | ||||||
|  |             keyboardType: formItem.textInputType, | ||||||
|             obscureText: formItem.password, |             obscureText: formItem.password, | ||||||
|             autocorrect: !formItem.password, |             autocorrect: !formItem.password, | ||||||
|             enableSuggestions: !formItem.password, |             enableSuggestions: !formItem.password, | ||||||
| @@ -370,34 +374,37 @@ class _GeneratedFormState extends State<GeneratedForm> { | |||||||
|                     }) ?? |                     }) ?? | ||||||
|                     [const SizedBox.shrink()], |                     [const SizedBox.shrink()], | ||||||
|                 (values[widget.items[r][e].key] |                 (values[widget.items[r][e].key] | ||||||
|                 as Map<String, MapEntry<int, bool>>?) |                                 as Map<String, MapEntry<int, bool>>?) | ||||||
|                     ?.values |                             ?.values | ||||||
|                     .where((e) => e.value) |                             .where((e) => e.value) | ||||||
|                     .length == 1 |                             .length == | ||||||
|  |                         1 | ||||||
|                     ? Padding( |                     ? Padding( | ||||||
|                     padding: const EdgeInsets.symmetric(horizontal: 4), |                         padding: const EdgeInsets.symmetric(horizontal: 4), | ||||||
|                     child: IconButton( |                         child: IconButton( | ||||||
|                       onPressed: () { |                           onPressed: () { | ||||||
|                         setState(() { |                             setState(() { | ||||||
|                           var temp = values[widget.items[r][e].key] |                               var temp = values[widget.items[r][e].key] | ||||||
|                               as Map<String, MapEntry<int, bool>>; |                                   as Map<String, MapEntry<int, bool>>; | ||||||
|                           // get selected category str where bool is true |                               // get selected category str where bool is true | ||||||
|                           final oldEntry = temp.entries.firstWhere((entry) => entry.value.value); |                               final oldEntry = temp.entries | ||||||
|                           // generate new color, ensure it is not the same |                                   .firstWhere((entry) => entry.value.value); | ||||||
|                           int newColor = oldEntry.value.key; |                               // generate new color, ensure it is not the same | ||||||
|                           while(oldEntry.value.key == newColor) { |                               int newColor = oldEntry.value.key; | ||||||
|                             newColor = generateRandomLightColor().value; |                               while (oldEntry.value.key == newColor) { | ||||||
|                           } |                                 newColor = generateRandomLightColor().value; | ||||||
|                           // Update entry with new color, remain selected |                               } | ||||||
|                           temp.update(oldEntry.key, (old) => MapEntry(newColor, old.value)); |                               // Update entry with new color, remain selected | ||||||
|                           values[widget.items[r][e].key] = temp; |                               temp.update(oldEntry.key, | ||||||
|                           someValueChanged(); |                                   (old) => MapEntry(newColor, old.value)); | ||||||
|                         }); |                               values[widget.items[r][e].key] = temp; | ||||||
|                       }, |                               someValueChanged(); | ||||||
|                       icon: const Icon(Icons.format_color_fill_rounded), |                             }); | ||||||
|                       visualDensity: VisualDensity.compact, |                           }, | ||||||
|                       tooltip: tr('colour'), |                           icon: const Icon(Icons.format_color_fill_rounded), | ||||||
|                     )) |                           visualDensity: VisualDensity.compact, | ||||||
|  |                           tooltip: tr('colour'), | ||||||
|  |                         )) | ||||||
|                     : const SizedBox.shrink(), |                     : const SizedBox.shrink(), | ||||||
|                 (values[widget.items[r][e].key] |                 (values[widget.items[r][e].key] | ||||||
|                                 as Map<String, MapEntry<int, bool>>?) |                                 as Map<String, MapEntry<int, bool>>?) | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; | |||||||
| // ignore: implementation_imports | // ignore: implementation_imports | ||||||
| import 'package:easy_localization/src/localization.dart'; | import 'package:easy_localization/src/localization.dart'; | ||||||
|  |  | ||||||
| const String currentVersion = '0.14.10'; | const String currentVersion = '0.14.16'; | ||||||
| const String currentReleaseTag = | const String currentReleaseTag = | ||||||
|     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES |     'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,8 +15,10 @@ class GitHubStars implements MassAppUrlSource { | |||||||
|  |  | ||||||
|   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( |   Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( | ||||||
|       String username, int page) async { |       String username, int page) async { | ||||||
|     Response res = await get(Uri.parse( |     Response res = await get( | ||||||
|         'https://${await GitHub().getCredentialPrefixIfAny({})}api.github.com/users/$username/starred?per_page=100&page=$page')); |         Uri.parse( | ||||||
|  |             'https://api.github.com/users/$username/starred?per_page=100&page=$page'), | ||||||
|  |         headers: await GitHub().getRequestHeaders()); | ||||||
|     if (res.statusCode == 200) { |     if (res.statusCode == 200) { | ||||||
|       Map<String, List<String>> urlsWithDescriptions = {}; |       Map<String, List<String>> urlsWithDescriptions = {}; | ||||||
|       for (var e in (jsonDecode(res.body) as List<dynamic>)) { |       for (var e in (jsonDecode(res.body) as List<dynamic>)) { | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:obtainium/pages/app.dart'; | |||||||
| import 'package:obtainium/pages/import_export.dart'; | import 'package:obtainium/pages/import_export.dart'; | ||||||
| import 'package:obtainium/pages/settings.dart'; | import 'package:obtainium/pages/settings.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
|  | import 'package:obtainium/providers/notifications_provider.dart'; | ||||||
| import 'package:obtainium/providers/settings_provider.dart'; | import 'package:obtainium/providers/settings_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| @@ -42,6 +43,8 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     AppsProvider appsProvider = context.read<AppsProvider>(); |     AppsProvider appsProvider = context.read<AppsProvider>(); | ||||||
|     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); |     SettingsProvider settingsProvider = context.watch<SettingsProvider>(); | ||||||
|  |     NotificationsProvider notificationsProvider = | ||||||
|  |         context.read<NotificationsProvider>(); | ||||||
|  |  | ||||||
|     bool doingSomething = gettingAppInfo || searching; |     bool doingSomething = gettingAppInfo || searching; | ||||||
|  |  | ||||||
| @@ -145,7 +148,7 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                 userPickedTrackOnly))) { |                 userPickedTrackOnly))) { | ||||||
|           var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; |           var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly; | ||||||
|           app = await sourceProvider.getApp( |           app = await sourceProvider.getApp( | ||||||
|               pickedSource!, userInput, additionalSettings, |               pickedSource!, userInput.trim(), additionalSettings, | ||||||
|               trackOnlyOverride: trackOnly, |               trackOnlyOverride: trackOnly, | ||||||
|               overrideSource: pickedSourceOverride, |               overrideSource: pickedSourceOverride, | ||||||
|               inferAppIdIfOptional: inferAppIdIfOptional); |               inferAppIdIfOptional: inferAppIdIfOptional); | ||||||
| @@ -161,7 +164,8 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); |                 app.apkUrls.map((e) => e.value).toList().indexOf(apkUrl.value); | ||||||
|             // ignore: use_build_context_synchronously |             // ignore: use_build_context_synchronously | ||||||
|             var downloadedArtifact = await appsProvider.downloadApp( |             var downloadedArtifact = await appsProvider.downloadApp( | ||||||
|                 app, globalNavigatorKey.currentContext); |                 app, globalNavigatorKey.currentContext, | ||||||
|  |                 notificationsProvider: notificationsProvider); | ||||||
|             DownloadedApk? downloadedFile; |             DownloadedApk? downloadedFile; | ||||||
|             DownloadedXApkDir? downloadedDir; |             DownloadedXApkDir? downloadedDir; | ||||||
|             if (downloadedArtifact is DownloadedApk) { |             if (downloadedArtifact is DownloadedApk) { | ||||||
| @@ -459,14 +463,12 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|             crossAxisAlignment: CrossAxisAlignment.center, |             crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|             mainAxisAlignment: MainAxisAlignment.center, |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|             children: [ |             children: [ | ||||||
|               const SizedBox( |  | ||||||
|                 height: 48, |  | ||||||
|               ), |  | ||||||
|               Text( |               Text( | ||||||
|                 tr('supportedSourcesBelow'), |                 tr('supportedSources'), | ||||||
|  |                 style: const TextStyle(fontWeight: FontWeight.bold), | ||||||
|               ), |               ), | ||||||
|               const SizedBox( |               const SizedBox( | ||||||
|                 height: 8, |                 height: 16, | ||||||
|               ), |               ), | ||||||
|               ...sourceProvider.sources |               ...sourceProvider.sources | ||||||
|                   .map((e) => GestureDetector( |                   .map((e) => GestureDetector( | ||||||
| @@ -520,15 +522,17 @@ class _AddAppPageState extends State<AddAppPage> { | |||||||
|                                   : const SizedBox(); |                                   : const SizedBox(); | ||||||
|                             }, |                             }, | ||||||
|                             future: pickedSource?.getSourceNote()), |                             future: pickedSource?.getSourceNote()), | ||||||
|                       const SizedBox( |                       SizedBox( | ||||||
|                         height: 16, |                         height: pickedSource != null ? 16 : 96, | ||||||
|                       ), |                       ), | ||||||
|                       if (pickedSource != null) |                       if (pickedSource != null) getAdditionalOptsCol(), | ||||||
|                         getAdditionalOptsCol() |                       if (pickedSource == null) | ||||||
|                       else |                         const Divider( | ||||||
|                         getSourcesListWidget(), |                           height: 48, | ||||||
|                       const SizedBox( |                         ), | ||||||
|                         height: 8, |                       if (pickedSource == null) getSourcesListWidget(), | ||||||
|  |                       SizedBox( | ||||||
|  |                         height: pickedSource != null ? 8 : 2, | ||||||
|                       ), |                       ), | ||||||
|                     ])), |                     ])), | ||||||
|           ) |           ) | ||||||
|   | |||||||
| @@ -152,7 +152,7 @@ class _AppPageState extends State<AppPage> { | |||||||
|           mainAxisAlignment: MainAxisAlignment.center, |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|           children: [ |           children: [ | ||||||
|             const SizedBox(height: 125), |             const SizedBox(height: 20), | ||||||
|             app?.icon != null |             app?.icon != null | ||||||
|                 ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ |                 ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ | ||||||
|                     Image.memory( |                     Image.memory( | ||||||
| @@ -338,9 +338,9 @@ class _AppPageState extends State<AppPage> { | |||||||
|                 try { |                 try { | ||||||
|                   HapticFeedback.heavyImpact(); |                   HapticFeedback.heavyImpact(); | ||||||
|                   var res = await appsProvider.downloadAndInstallLatestApps( |                   var res = await appsProvider.downloadAndInstallLatestApps( | ||||||
|                       app?.app.id != null ? [app!.app.id] : [], |                     app?.app.id != null ? [app!.app.id] : [], | ||||||
|                       globalNavigatorKey.currentContext, |                     globalNavigatorKey.currentContext, | ||||||
|                       settingsProvider); |                   ); | ||||||
|                   if (app?.app.installedVersion != null && !trackOnly) { |                   if (app?.app.installedVersion != null && !trackOnly) { | ||||||
|                     // ignore: use_build_context_synchronously |                     // ignore: use_build_context_synchronously | ||||||
|                     showError(tr('appsUpdated'), context); |                     showError(tr('appsUpdated'), context); | ||||||
| @@ -463,9 +463,18 @@ class _AppPageState extends State<AppPage> { | |||||||
|                           : null)) |                           : null)) | ||||||
|           ], |           ], | ||||||
|         )); |         )); | ||||||
|  |      | ||||||
|  |     appScreenAppBar() => AppBar( | ||||||
|  |       leading: IconButton( | ||||||
|  |         icon: const Icon(Icons.arrow_back), | ||||||
|  |         onPressed: () { | ||||||
|  |           Navigator.pop(context); | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|         appBar: settingsProvider.showAppWebpage ? AppBar() : null, |         appBar: settingsProvider.showAppWebpage ? AppBar() : appScreenAppBar(), | ||||||
|         backgroundColor: Theme.of(context).colorScheme.surface, |         backgroundColor: Theme.of(context).colorScheme.surface, | ||||||
|         body: RefreshIndicator( |         body: RefreshIndicator( | ||||||
|             child: settingsProvider.showAppWebpage |             child: settingsProvider.showAppWebpage | ||||||
|   | |||||||
| @@ -381,8 +381,7 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               : () { |               : () { | ||||||
|                   appsProvider.downloadAndInstallLatestApps( |                   appsProvider.downloadAndInstallLatestApps( | ||||||
|                       [listedApps[appIndex].app.id], |                       [listedApps[appIndex].app.id], | ||||||
|                       globalNavigatorKey.currentContext, |                       globalNavigatorKey.currentContext).catchError((e) { | ||||||
|                       settingsProvider).catchError((e) { |  | ||||||
|                     showError(e, context); |                     showError(e, context); | ||||||
|                     return <String>[]; |                     return <String>[]; | ||||||
|                   }); |                   }); | ||||||
| @@ -449,33 +448,48 @@ class AppsPageState extends State<AppsPage> { | |||||||
|               : const SizedBox.shrink(), |               : const SizedBox.shrink(), | ||||||
|           GestureDetector( |           GestureDetector( | ||||||
|               onTap: showChangesFn, |               onTap: showChangesFn, | ||||||
|               child: Column( |               child: Container( | ||||||
|                 mainAxisAlignment: MainAxisAlignment.center, |                   decoration: BoxDecoration( | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.end, |                       borderRadius: BorderRadius.circular(12), | ||||||
|                 children: [ |                       color: settingsProvider.highlightTouchTargets && | ||||||
|                   Row(mainAxisSize: MainAxisSize.min, children: [ |                               showChangesFn != null | ||||||
|                     Container( |                           ? (Theme.of(context).brightness == Brightness.light | ||||||
|                         constraints: BoxConstraints( |                                   ? Theme.of(context).primaryColor | ||||||
|                             maxWidth: MediaQuery.of(context).size.width / 4), |                                   : Theme.of(context).primaryColorLight) | ||||||
|                         child: Text(getVersionText(index), |                               .withAlpha(20) | ||||||
|                             overflow: TextOverflow.ellipsis, |                           : null), | ||||||
|                             textAlign: TextAlign.end)), |                   padding: settingsProvider.highlightTouchTargets | ||||||
|                   ]), |                       ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0) | ||||||
|                   Row( |                       : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0), | ||||||
|                     mainAxisSize: MainAxisSize.min, |                   child: Column( | ||||||
|  |                     mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Text( |                       Row(mainAxisSize: MainAxisSize.min, children: [ | ||||||
|                         getChangesButtonString(index, showChangesFn != null), |                         Container( | ||||||
|                         style: TextStyle( |                             constraints: BoxConstraints( | ||||||
|                             fontStyle: FontStyle.italic, |                                 maxWidth: | ||||||
|                             decoration: showChangesFn != null |                                     MediaQuery.of(context).size.width / 4), | ||||||
|                                 ? TextDecoration.underline |                             child: Text(getVersionText(index), | ||||||
|                                 : TextDecoration.none), |                                 overflow: TextOverflow.ellipsis, | ||||||
|                       ) |                                 textAlign: TextAlign.end)), | ||||||
|  |                       ]), | ||||||
|  |                       Row( | ||||||
|  |                         mainAxisSize: MainAxisSize.min, | ||||||
|  |                         children: [ | ||||||
|  |                           Text( | ||||||
|  |                             getChangesButtonString( | ||||||
|  |                                 index, showChangesFn != null), | ||||||
|  |                             style: TextStyle( | ||||||
|  |                                 fontStyle: FontStyle.italic, | ||||||
|  |                                 decoration: showChangesFn != null | ||||||
|  |                                     ? TextDecoration.underline | ||||||
|  |                                     : TextDecoration.none), | ||||||
|  |                           ) | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ))) | ||||||
|                 ], |  | ||||||
|               )) |  | ||||||
|         ], |         ], | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
| @@ -684,8 +698,8 @@ class AppsPageState extends State<AppsPage> { | |||||||
|                     toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); |                     toInstall.addAll(trackOnlyUpdateIdsAllOrSelected); | ||||||
|                   } |                   } | ||||||
|                   appsProvider |                   appsProvider | ||||||
|                       .downloadAndInstallLatestApps(toInstall, |                       .downloadAndInstallLatestApps( | ||||||
|                           globalNavigatorKey.currentContext, settingsProvider) |                           toInstall, globalNavigatorKey.currentContext) | ||||||
|                       .catchError((e) { |                       .catchError((e) { | ||||||
|                     showError(e, context); |                     showError(e, context); | ||||||
|                     return <String>[]; |                     return <String>[]; | ||||||
|   | |||||||
| @@ -28,8 +28,8 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     SourceProvider sourceProvider = SourceProvider(); |     SourceProvider sourceProvider = SourceProvider(); | ||||||
|     var appsProvider = context.read<AppsProvider>(); |     var appsProvider = context.watch<AppsProvider>(); | ||||||
|     var settingsProvider = context.read<SettingsProvider>(); |     var settingsProvider = context.watch<SettingsProvider>(); | ||||||
|  |  | ||||||
|     var outlineButtonStyle = ButtonStyle( |     var outlineButtonStyle = ButtonStyle( | ||||||
|       shape: MaterialStateProperty.all( |       shape: MaterialStateProperty.all( | ||||||
| @@ -102,10 +102,16 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     runObtainiumExport() { |     runObtainiumExport() async { | ||||||
|       HapticFeedback.selectionClick(); |       HapticFeedback.selectionClick(); | ||||||
|       appsProvider.exportApps().then((String path) { |       appsProvider | ||||||
|         showError(tr('exportedTo', args: [path]), context); |           .exportApps( | ||||||
|  |               pickOnly: (await settingsProvider.getExportDir()) == null, | ||||||
|  |               sp: settingsProvider) | ||||||
|  |           .then((String? result) { | ||||||
|  |         if (result != null) { | ||||||
|  |           showError(tr('exportedTo', args: [result]), context); | ||||||
|  |         } | ||||||
|       }).catchError((e) { |       }).catchError((e) { | ||||||
|         showError(e, context); |         showError(e, context); | ||||||
|       }); |       }); | ||||||
| @@ -301,27 +307,68 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                   child: Column( |                   child: Column( | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, |                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Row( |                       FutureBuilder( | ||||||
|                         children: [ |                         future: settingsProvider.getExportDir(), | ||||||
|                           Expanded( |                         builder: (context, snapshot) { | ||||||
|                               child: TextButton( |                           return Column( | ||||||
|                                   style: outlineButtonStyle, |                             children: [ | ||||||
|                                   onPressed: appsProvider.apps.isEmpty || |                               Row( | ||||||
|                                           importInProgress |                                 children: [ | ||||||
|                                       ? null |                                   Expanded( | ||||||
|                                       : runObtainiumExport, |                                       child: TextButton( | ||||||
|                                   child: Text(tr('obtainiumExport')))), |                                     style: outlineButtonStyle, | ||||||
|                           const SizedBox( |                                     onPressed: appsProvider.apps.isEmpty || | ||||||
|                             width: 16, |                                             importInProgress | ||||||
|                           ), |                                         ? null | ||||||
|                           Expanded( |                                         : runObtainiumExport, | ||||||
|                               child: TextButton( |                                     child: Text(tr(snapshot.data != null | ||||||
|                                   style: outlineButtonStyle, |                                         ? 'obtainiumExport' | ||||||
|                                   onPressed: importInProgress |                                         : 'pickExportDir')), | ||||||
|                                       ? null |                                   )), | ||||||
|                                       : runObtainiumImport, |                                   const SizedBox( | ||||||
|                                   child: Text(tr('obtainiumImport')))) |                                     width: 16, | ||||||
|                         ], |                                   ), | ||||||
|  |                                   Expanded( | ||||||
|  |                                       child: TextButton( | ||||||
|  |                                           style: outlineButtonStyle, | ||||||
|  |                                           onPressed: importInProgress | ||||||
|  |                                               ? null | ||||||
|  |                                               : runObtainiumImport, | ||||||
|  |                                           child: Text(tr('obtainiumImport')))) | ||||||
|  |                                 ], | ||||||
|  |                               ), | ||||||
|  |                               if (snapshot.data != null) | ||||||
|  |                                 Column( | ||||||
|  |                                   children: [ | ||||||
|  |                                     const SizedBox(height: 16), | ||||||
|  |                                     GeneratedForm( | ||||||
|  |                                         items: [ | ||||||
|  |                                           [ | ||||||
|  |                                             GeneratedFormSwitch( | ||||||
|  |                                               'autoExportOnChanges', | ||||||
|  |                                               label: tr('autoExportOnChanges'), | ||||||
|  |                                               defaultValue: settingsProvider | ||||||
|  |                                                   .autoExportOnChanges, | ||||||
|  |                                             ) | ||||||
|  |                                           ] | ||||||
|  |                                         ], | ||||||
|  |                                         onValueChanges: | ||||||
|  |                                             (value, valid, isBuilding) { | ||||||
|  |                                           if (valid && !isBuilding) { | ||||||
|  |                                             if (value['autoExportOnChanges'] != | ||||||
|  |                                                 null) { | ||||||
|  |                                               settingsProvider | ||||||
|  |                                                   .autoExportOnChanges = value[ | ||||||
|  |                                                       'autoExportOnChanges'] == | ||||||
|  |                                                   true; | ||||||
|  |                                             } | ||||||
|  |                                           } | ||||||
|  |                                         }), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                             ], | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|                       ), |                       ), | ||||||
|                       if (importInProgress) |                       if (importInProgress) | ||||||
|                         const Column( |                         const Column( | ||||||
| @@ -399,7 +446,7 @@ class _ImportExportPageState extends State<ImportExportPage> { | |||||||
|                               fontStyle: FontStyle.italic, fontSize: 12)), |                               fontStyle: FontStyle.italic, fontSize: 12)), | ||||||
|                       const SizedBox( |                       const SizedBox( | ||||||
|                         height: 8, |                         height: 8, | ||||||
|                       ) |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ))) |                   ))) | ||||||
|         ])); |         ])); | ||||||
|   | |||||||
| @@ -484,6 +484,21 @@ class _SettingsPageState extends State<SettingsPage> { | |||||||
|                                           }) |                                           }) | ||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|  |                             height16, | ||||||
|  |                             Row( | ||||||
|  |                               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                               children: [ | ||||||
|  |                                 Flexible( | ||||||
|  |                                     child: Text(tr('highlightTouchTargets'))), | ||||||
|  |                                 Switch( | ||||||
|  |                                     value: | ||||||
|  |                                         settingsProvider.highlightTouchTargets, | ||||||
|  |                                     onChanged: (value) { | ||||||
|  |                                       settingsProvider.highlightTouchTargets = | ||||||
|  |                                           value; | ||||||
|  |                                     }) | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|                             height32, |                             height32, | ||||||
|                             Text( |                             Text( | ||||||
|                               tr('categories'), |                               tr('categories'), | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import 'dart:async'; | |||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
| import 'dart:math'; | import 'dart:math'; | ||||||
|  | import 'package:http/http.dart' as http; | ||||||
|  |  | ||||||
| import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; | ||||||
| import 'package:android_intent_plus/flag.dart'; | import 'package:android_intent_plus/flag.dart'; | ||||||
| @@ -30,6 +31,7 @@ import 'package:obtainium/providers/source_provider.dart'; | |||||||
| import 'package:http/http.dart'; | import 'package:http/http.dart'; | ||||||
| import 'package:android_intent_plus/android_intent.dart'; | import 'package:android_intent_plus/android_intent.dart'; | ||||||
| import 'package:flutter_archive/flutter_archive.dart'; | import 'package:flutter_archive/flutter_archive.dart'; | ||||||
|  | import 'package:shared_storage/shared_storage.dart' as saf; | ||||||
|  |  | ||||||
| final pm = AndroidPackageManager(); | final pm = AndroidPackageManager(); | ||||||
|  |  | ||||||
| @@ -149,6 +151,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|   late Stream<FGBGType>? foregroundStream; |   late Stream<FGBGType>? foregroundStream; | ||||||
|   late StreamSubscription<FGBGType>? foregroundSubscription; |   late StreamSubscription<FGBGType>? foregroundSubscription; | ||||||
|   late Directory APKDir; |   late Directory APKDir; | ||||||
|  |   late SettingsProvider settingsProvider = SettingsProvider(); | ||||||
|  |  | ||||||
|   Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); |   Iterable<AppInMemory> getAppValues() => apps.values.map((a) => a.deepCopy()); | ||||||
|  |  | ||||||
| @@ -160,6 +163,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|       if (isForeground) await loadApps(); |       if (isForeground) await loadApps(); | ||||||
|     }); |     }); | ||||||
|     () async { |     () async { | ||||||
|  |       await settingsProvider.initializeSettings(); | ||||||
|       var cacheDirs = await getExternalCacheDirectories(); |       var cacheDirs = await getExternalCacheDirectories(); | ||||||
|       if (cacheDirs?.isNotEmpty ?? false) { |       if (cacheDirs?.isNotEmpty ?? false) { | ||||||
|         APKDir = cacheDirs!.first; |         APKDir = cacheDirs!.first; | ||||||
| @@ -215,7 +219,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|     if (headers != null) { |     if (headers != null) { | ||||||
|       req.headers.addAll(headers); |       req.headers.addAll(headers); | ||||||
|     } |     } | ||||||
|     var client = Client(); |     var client = http.Client(); | ||||||
|     StreamedResponse response = await client.send(req); |     StreamedResponse response = await client.send(req); | ||||||
|     String ext = |     String ext = | ||||||
|         response.headers['content-disposition']?.split('.').last ?? 'apk'; |         response.headers['content-disposition']?.split('.').last ?? 'apk'; | ||||||
| @@ -298,9 +302,11 @@ class AppsProvider with ChangeNotifier { | |||||||
|       notificationsProvider?.cancel(notif.id); |       notificationsProvider?.cancel(notif.id); | ||||||
|       int? prevProg; |       int? prevProg; | ||||||
|       var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; |       var fileNameNoExt = '${app.id}-${downloadUrl.hashCode}'; | ||||||
|  |       var headers = await source.getRequestHeaders( | ||||||
|  |           additionalSettings: app.additionalSettings, forAPKDownload: true); | ||||||
|       var downloadedFile = await downloadFileWithRetry( |       var downloadedFile = await downloadFileWithRetry( | ||||||
|           downloadUrl, fileNameNoExt, headers: source.requestHeaders, |           downloadUrl, fileNameNoExt, | ||||||
|           (double? progress) { |           headers: headers, (double? progress) { | ||||||
|         int? prog = progress?.ceil(); |         int? prog = progress?.ceil(); | ||||||
|         if (apps[app.id] != null) { |         if (apps[app.id] != null) { | ||||||
|           apps[app.id]!.downloadProgress = progress; |           apps[app.id]!.downloadProgress = progress; | ||||||
| @@ -366,8 +372,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|       .where((element) => element.downloadProgress != null) |       .where((element) => element.downloadProgress != null) | ||||||
|       .isNotEmpty; |       .isNotEmpty; | ||||||
|  |  | ||||||
|   Future<bool> canInstallSilently( |   Future<bool> canInstallSilently(App app) async { | ||||||
|       App app, SettingsProvider settingsProvider) async { |  | ||||||
|     if (app.id == obtainiumId) { |     if (app.id == obtainiumId) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| @@ -425,7 +430,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|         zipFile: File(filePath), destinationDir: Directory(destinationPath)); |         zipFile: File(filePath), destinationDir: Directory(destinationPath)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> installXApkDir(DownloadedXApkDir dir) async { |   Future<void> installXApkDir(DownloadedXApkDir dir, | ||||||
|  |       {bool needsBGWorkaround = false}) async { | ||||||
|     // We don't know which APKs in an XAPK are supported by the user's device |     // We don't know which APKs in an XAPK are supported by the user's device | ||||||
|     // So we try installing all of them and assume success if at least one installed |     // So we try installing all of them and assume success if at least one installed | ||||||
|     // If 0 APKs installed, throw the first install error encountered |     // If 0 APKs installed, throw the first install error encountered | ||||||
| @@ -438,7 +444,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|         if (file.path.toLowerCase().endsWith('.apk')) { |         if (file.path.toLowerCase().endsWith('.apk')) { | ||||||
|           try { |           try { | ||||||
|             somethingInstalled = somethingInstalled || |             somethingInstalled = somethingInstalled || | ||||||
|                 await installApk(DownloadedApk(dir.appId, file)); |                 await installApk(DownloadedApk(dir.appId, file), | ||||||
|  |                     needsBGWorkaround: needsBGWorkaround); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             logs.add( |             logs.add( | ||||||
|                 'Could not install APK from XAPK \'${file.path}\': ${e.toString()}'); |                 'Could not install APK from XAPK \'${file.path}\': ${e.toString()}'); | ||||||
| @@ -458,7 +465,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<bool> installApk(DownloadedApk file) async { |   Future<bool> installApk(DownloadedApk file, | ||||||
|  |       {bool needsBGWorkaround = false}) async { | ||||||
|     var newInfo = |     var newInfo = | ||||||
|         await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); |         await pm.getPackageArchiveInfo(archiveFilePath: file.file.path); | ||||||
|     PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); |     PackageInfo? appInfo = await getInstalledInfo(apps[file.appId]!.app.id); | ||||||
| @@ -467,6 +475,17 @@ class AppsProvider with ChangeNotifier { | |||||||
|         !(await canDowngradeApps())) { |         !(await canDowngradeApps())) { | ||||||
|       throw DowngradeError(); |       throw DowngradeError(); | ||||||
|     } |     } | ||||||
|  |     if (needsBGWorkaround) { | ||||||
|  |       // The below 'await' will never return if we are in a background process | ||||||
|  |       // To work around this, we should assume the install will be successful | ||||||
|  |       // So we update the app's installed version first as we will never get to the later code | ||||||
|  |       // We can't conditionally get rid of the 'await' as this causes install fails (BG process times out) - see #896 | ||||||
|  |       // TODO: When fixed, update this function and the calls to it accordingly | ||||||
|  |       apps[file.appId]!.app.installedVersion = | ||||||
|  |           apps[file.appId]!.app.latestVersion; | ||||||
|  |       await saveApps([apps[file.appId]!.app], | ||||||
|  |           attemptToCorrectInstallStatus: false); | ||||||
|  |     } | ||||||
|     int? code = |     int? code = | ||||||
|         await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); |         await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); | ||||||
|     bool installed = false; |     bool installed = false; | ||||||
| @@ -536,7 +555,6 @@ class AppsProvider with ChangeNotifier { | |||||||
|         getHost(apkUrl.value) != getHost(app.url) && |         getHost(apkUrl.value) != getHost(app.url) && | ||||||
|         context != null) { |         context != null) { | ||||||
|       // ignore: use_build_context_synchronously |       // ignore: use_build_context_synchronously | ||||||
|       var settingsProvider = context.read<SettingsProvider>(); |  | ||||||
|       if (!(settingsProvider.hideAPKOriginWarning) && |       if (!(settingsProvider.hideAPKOriginWarning) && | ||||||
|           // ignore: use_build_context_synchronously |           // ignore: use_build_context_synchronously | ||||||
|           await showDialog( |           await showDialog( | ||||||
| @@ -557,8 +575,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|   // If no BuildContext is provided, apps that require user interaction are ignored |   // If no BuildContext is provided, apps that require user interaction are ignored | ||||||
|   // If user input is needed and the App is in the background, a notification is sent to get the user's attention |   // If user input is needed and the App is in the background, a notification is sent to get the user's attention | ||||||
|   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result |   // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result | ||||||
|   Future<List<String>> downloadAndInstallLatestApps(List<String> appIds, |   Future<List<String>> downloadAndInstallLatestApps( | ||||||
|       BuildContext? context, SettingsProvider settingsProvider, |       List<String> appIds, BuildContext? context, | ||||||
|       {NotificationsProvider? notificationsProvider}) async { |       {NotificationsProvider? notificationsProvider}) async { | ||||||
|     notificationsProvider = |     notificationsProvider = | ||||||
|         notificationsProvider ?? context?.read<NotificationsProvider>(); |         notificationsProvider ?? context?.read<NotificationsProvider>(); | ||||||
| @@ -587,8 +605,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|           apps[id]!.app.preferredApkIndex = urlInd; |           apps[id]!.app.preferredApkIndex = urlInd; | ||||||
|           await saveApps([apps[id]!.app]); |           await saveApps([apps[id]!.app]); | ||||||
|         } |         } | ||||||
|         if (context != null || |         if (context != null || await canInstallSilently(apps[id]!.app)) { | ||||||
|             await canInstallSilently(apps[id]!.app, settingsProvider)) { |  | ||||||
|           appsToInstall.add(id); |           appsToInstall.add(id); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -625,8 +642,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|           downloadedDir = downloadedArtifact as DownloadedXApkDir; |           downloadedDir = downloadedArtifact as DownloadedXApkDir; | ||||||
|         } |         } | ||||||
|         var appId = downloadedFile?.appId ?? downloadedDir!.appId; |         var appId = downloadedFile?.appId ?? downloadedDir!.appId; | ||||||
|         bool willBeSilent = |         bool willBeSilent = await canInstallSilently(apps[appId]!.app); | ||||||
|             await canInstallSilently(apps[appId]!.app, settingsProvider); |  | ||||||
|         if (!(await settingsProvider.getInstallPermission(enforce: false))) { |         if (!(await settingsProvider.getInstallPermission(enforce: false))) { | ||||||
|           throw ObtainiumError(tr('cancelled')); |           throw ObtainiumError(tr('cancelled')); | ||||||
|         } |         } | ||||||
| @@ -639,15 +655,13 @@ class AppsProvider with ChangeNotifier { | |||||||
|         try { |         try { | ||||||
|           if (downloadedFile != null) { |           if (downloadedFile != null) { | ||||||
|             if (willBeSilent && context == null) { |             if (willBeSilent && context == null) { | ||||||
|               // Would await forever - workaround - TODO |               installApk(downloadedFile, needsBGWorkaround: true); | ||||||
|               installApk(downloadedFile); |  | ||||||
|             } else { |             } else { | ||||||
|               await installApk(downloadedFile); |               await installApk(downloadedFile); | ||||||
|             } |             } | ||||||
|           } else { |           } else { | ||||||
|             if (willBeSilent && context == null) { |             if (willBeSilent && context == null) { | ||||||
|               // Would await forever - workaround - TODO |               installXApkDir(downloadedDir!, needsBGWorkaround: true); | ||||||
|               installXApkDir(downloadedDir!); |  | ||||||
|             } else { |             } else { | ||||||
|               await installXApkDir(downloadedDir!); |               await installXApkDir(downloadedDir!); | ||||||
|             } |             } | ||||||
| @@ -675,8 +689,8 @@ class AppsProvider with ChangeNotifier { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<Directory> getAppsDir() async { |   Future<Directory> getAppsDir() async { | ||||||
|     Directory appsDir = Directory( |     Directory appsDir = | ||||||
|         '${(await getExternalStorageDirectory())?.path as String}/app_data'); |         Directory('${(await getExternalStorageDirectory())!.path}/app_data'); | ||||||
|     if (!appsDir.existsSync()) { |     if (!appsDir.existsSync()) { | ||||||
|       appsDir.createSync(); |       appsDir.createSync(); | ||||||
|     } |     } | ||||||
| @@ -876,8 +890,6 @@ class AppsProvider with ChangeNotifier { | |||||||
|           .toList(); |           .toList(); | ||||||
|       // After reconciliation, delete externally uninstalled Apps if needed |       // After reconciliation, delete externally uninstalled Apps if needed | ||||||
|       if (removedAppIds.isNotEmpty) { |       if (removedAppIds.isNotEmpty) { | ||||||
|         var settingsProvider = SettingsProvider(); |  | ||||||
|         await settingsProvider.initializeSettings(); |  | ||||||
|         if (settingsProvider.removeOnExternalUninstall) { |         if (settingsProvider.removeOnExternalUninstall) { | ||||||
|           await removeApps(removedAppIds); |           await removeApps(removedAppIds); | ||||||
|         } |         } | ||||||
| @@ -916,6 +928,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|  |     exportApps(isAuto: true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> removeApps(List<String> appIds) async { |   Future<void> removeApps(List<String> appIds) async { | ||||||
| @@ -937,6 +950,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|     if (appIds.isNotEmpty) { |     if (appIds.isNotEmpty) { | ||||||
|       notifyListeners(); |       notifyListeners(); | ||||||
|  |       exportApps(isAuto: true); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -1093,32 +1107,52 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return updateAppIds; |     return updateAppIds; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<String> exportApps() async { |   Future<String?> exportApps( | ||||||
|     if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 29) { |       {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async { | ||||||
|       if (await Permission.storage.isDenied) { |     SettingsProvider settingsProvider = sp ?? this.settingsProvider; | ||||||
|         await Permission.storage.request(); |     var exportDir = await settingsProvider.getExportDir(); | ||||||
|  |     if (isAuto) { | ||||||
|  |       if (settingsProvider.autoExportOnChanges != true) { | ||||||
|  |         return null; | ||||||
|       } |       } | ||||||
|       if (await Permission.storage.isDenied) { |       if (exportDir == null) { | ||||||
|         throw ObtainiumError(tr('storagePermissionDenied')); |         logs.add('Skipping auto-export as dir is not set.'); | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |       logs.add('Started auto-export.'); | ||||||
|  |       var files = await saf | ||||||
|  |           .listFiles(exportDir, columns: [saf.DocumentFileColumn.id]) | ||||||
|  |           .where((f) => f.uri.pathSegments.last.endsWith('-auto.json')) | ||||||
|  |           .toList(); | ||||||
|  |       if (files.isNotEmpty) { | ||||||
|  |         for (var f in files) { | ||||||
|  |           saf.delete(f.uri); | ||||||
|  |         } | ||||||
|  |         logs.add('Previous auto-export deleted.'); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     Directory? exportDir = Directory('/storage/emulated/0/Download'); |     if (exportDir == null || pickOnly) { | ||||||
|     String path = 'Downloads'; // TODO: See if hardcoding this can be avoided |       await settingsProvider.pickExportDir(); | ||||||
|     var downloadsAccessible = false; |       exportDir = await settingsProvider.getExportDir(); | ||||||
|     try { |  | ||||||
|       downloadsAccessible = exportDir.existsSync(); |  | ||||||
|     } catch (e) { |  | ||||||
|       logs.add('Error accessing Downloads (will use fallback): $e'); |  | ||||||
|     } |     } | ||||||
|     if (!downloadsAccessible) { |     if (exportDir == null) { | ||||||
|       exportDir = await getExternalStorageDirectory(); |       return null; | ||||||
|       path = exportDir!.path; |  | ||||||
|     } |     } | ||||||
|     File export = File( |     String? returnPath; | ||||||
|         '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); |     if (!pickOnly) { | ||||||
|     export.writeAsStringSync( |       var result = await saf.createFile(exportDir, | ||||||
|         jsonEncode(apps.values.map((e) => e.app.toJson()).toList())); |           displayName: | ||||||
|     return path; |               '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', | ||||||
|  |           mimeType: 'application/json', | ||||||
|  |           bytes: Uint8List.fromList(utf8.encode( | ||||||
|  |               jsonEncode(apps.values.map((e) => e.app.toJson()).toList())))); | ||||||
|  |       if (result == null) { | ||||||
|  |         throw ObtainiumError(tr('unexpectedError')); | ||||||
|  |       } | ||||||
|  |       returnPath = | ||||||
|  |           exportDir.pathSegments.join('/').replaceFirst('tree/primary:', '/'); | ||||||
|  |     } | ||||||
|  |     return returnPath; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<int> importApps(String appsJSON) async { |   Future<int> importApps(String appsJSON) async { | ||||||
| @@ -1295,14 +1329,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | |||||||
|   NotificationsProvider notificationsProvider = NotificationsProvider(); |   NotificationsProvider notificationsProvider = NotificationsProvider(); | ||||||
|   AppsProvider appsProvider = AppsProvider(isBg: true); |   AppsProvider appsProvider = AppsProvider(isBg: true); | ||||||
|   await appsProvider.loadApps(); |   await appsProvider.loadApps(); | ||||||
|   var settingsProvider = SettingsProvider(); |  | ||||||
|   await settingsProvider.initializeSettings(); |  | ||||||
|  |  | ||||||
|   int maxAttempts = 4; |   int maxAttempts = 4; | ||||||
|  |  | ||||||
|   params ??= {}; |   params ??= {}; | ||||||
|   if (params['toCheck'] == null) { |   if (params['toCheck'] == null) { | ||||||
|     settingsProvider.lastBGCheckTime = DateTime.now(); |     appsProvider.settingsProvider.lastBGCheckTime = DateTime.now(); | ||||||
|   } |   } | ||||||
|   List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[ |   List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[ | ||||||
|     ...(params['toCheck'] |     ...(params['toCheck'] | ||||||
| @@ -1332,7 +1364,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | |||||||
|     var didCompleteChecking = false; |     var didCompleteChecking = false; | ||||||
|     CheckingUpdatesNotification? notif; |     CheckingUpdatesNotification? notif; | ||||||
|     var networkRestricted = false; |     var networkRestricted = false; | ||||||
|     if (settingsProvider.bgUpdatesOnWiFiOnly) { |     if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) { | ||||||
|       var netResult = await (Connectivity().checkConnectivity()); |       var netResult = await (Connectivity().checkConnectivity()); | ||||||
|       networkRestricted = (netResult != ConnectivityResult.wifi) && |       networkRestricted = (netResult != ConnectivityResult.wifi) && | ||||||
|           (netResult != ConnectivityResult.ethernet); |           (netResult != ConnectivityResult.ethernet); | ||||||
| @@ -1352,8 +1384,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | |||||||
|             App? newApp = await appsProvider.checkUpdate(appId); |             App? newApp = await appsProvider.checkUpdate(appId); | ||||||
|             if (newApp != null) { |             if (newApp != null) { | ||||||
|               if (networkRestricted || |               if (networkRestricted || | ||||||
|                   !(await appsProvider.canInstallSilently( |                   !(await appsProvider.canInstallSilently(app!.app))) { | ||||||
|                       app!.app, settingsProvider))) { |  | ||||||
|                 toNotify.add(newApp); |                 toNotify.add(newApp); | ||||||
|               } else { |               } else { | ||||||
|                 toInstall.add(MapEntry(appId, 0)); |                 toInstall.add(MapEntry(appId, 0)); | ||||||
| @@ -1439,8 +1470,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async { | |||||||
|       try { |       try { | ||||||
|         logs.add( |         logs.add( | ||||||
|             'BG install task $taskId: Attempting to update $appId in the background.'); |             'BG install task $taskId: Attempting to update $appId in the background.'); | ||||||
|         await appsProvider.downloadAndInstallLatestApps( |         await appsProvider.downloadAndInstallLatestApps([appId], null, | ||||||
|             [appId], null, settingsProvider, |  | ||||||
|             notificationsProvider: notificationsProvider); |             notificationsProvider: notificationsProvider); | ||||||
|         await Future.delayed(const Duration( |         await Future.delayed(const Duration( | ||||||
|             seconds: |             seconds: | ||||||
|   | |||||||
| @@ -9,8 +9,10 @@ import 'package:obtainium/app_sources/github.dart'; | |||||||
| import 'package:obtainium/main.dart'; | import 'package:obtainium/main.dart'; | ||||||
| import 'package:obtainium/providers/apps_provider.dart'; | import 'package:obtainium/providers/apps_provider.dart'; | ||||||
| import 'package:obtainium/providers/source_provider.dart'; | import 'package:obtainium/providers/source_provider.dart'; | ||||||
|  | import 'package:path_provider/path_provider.dart'; | ||||||
| import 'package:permission_handler/permission_handler.dart'; | import 'package:permission_handler/permission_handler.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|  | import 'package:shared_storage/shared_storage.dart' as saf; | ||||||
|  |  | ||||||
| String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; | String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; | ||||||
| String obtainiumId = 'dev.imranr.obtainium'; | String obtainiumId = 'dev.imranr.obtainium'; | ||||||
| @@ -35,6 +37,7 @@ List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0] | |||||||
|  |  | ||||||
| class SettingsProvider with ChangeNotifier { | class SettingsProvider with ChangeNotifier { | ||||||
|   SharedPreferences? prefs; |   SharedPreferences? prefs; | ||||||
|  |   String? defaultAppDir; | ||||||
|   bool justStarted = true; |   bool justStarted = true; | ||||||
|  |  | ||||||
|   String sourceUrl = 'https://github.com/ImranR98/Obtainium'; |   String sourceUrl = 'https://github.com/ImranR98/Obtainium'; | ||||||
| @@ -42,6 +45,7 @@ class SettingsProvider with ChangeNotifier { | |||||||
|   // Not done in constructor as we want to be able to await it |   // Not done in constructor as we want to be able to await it | ||||||
|   Future<void> initializeSettings() async { |   Future<void> initializeSettings() async { | ||||||
|     prefs = await SharedPreferences.getInstance(); |     prefs = await SharedPreferences.getInstance(); | ||||||
|  |     defaultAppDir = (await getExternalStorageDirectory())!.path; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -348,4 +352,58 @@ class SettingsProvider with ChangeNotifier { | |||||||
|     prefs?.setBool('showDebugOpts', val); |     prefs?.setBool('showDebugOpts', val); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   bool get highlightTouchTargets { | ||||||
|  |     return prefs?.getBool('highlightTouchTargets') ?? false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set highlightTouchTargets(bool val) { | ||||||
|  |     prefs?.setBool('highlightTouchTargets', val); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<Uri?> getExportDir() async { | ||||||
|  |     var uriString = prefs?.getString('exportDir'); | ||||||
|  |     if (uriString != null) { | ||||||
|  |       Uri? uri = Uri.parse(uriString); | ||||||
|  |       if (!(await saf.canRead(uri) ?? false) || | ||||||
|  |           !(await saf.canWrite(uri) ?? false)) { | ||||||
|  |         uri = null; | ||||||
|  |         prefs?.remove('exportDir'); | ||||||
|  |         notifyListeners(); | ||||||
|  |       } | ||||||
|  |       return uri; | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> pickExportDir({bool remove = false}) async { | ||||||
|  |     var existingSAFPerms = (await saf.persistedUriPermissions()) ?? []; | ||||||
|  |     var currentOneWayDataSyncDir = await getExportDir(); | ||||||
|  |     Uri? newOneWayDataSyncDir; | ||||||
|  |     if (!remove) { | ||||||
|  |       newOneWayDataSyncDir = (await saf.openDocumentTree()); | ||||||
|  |     } | ||||||
|  |     if (currentOneWayDataSyncDir?.path != newOneWayDataSyncDir?.path) { | ||||||
|  |       if (newOneWayDataSyncDir == null) { | ||||||
|  |         prefs?.remove('exportDir'); | ||||||
|  |       } else { | ||||||
|  |         prefs?.setString('exportDir', newOneWayDataSyncDir.toString()); | ||||||
|  |       } | ||||||
|  |       notifyListeners(); | ||||||
|  |     } | ||||||
|  |     for (var e in existingSAFPerms) { | ||||||
|  |       await saf.releasePersistableUriPermission(e.uri); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bool get autoExportOnChanges { | ||||||
|  |     return prefs?.getBool('autoExportOnChanges') ?? false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set autoExportOnChanges(bool val) { | ||||||
|  |     prefs?.setBool('autoExportOnChanges', val); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart'; | |||||||
| import 'package:obtainium/app_sources/sourcehut.dart'; | import 'package:obtainium/app_sources/sourcehut.dart'; | ||||||
| import 'package:obtainium/app_sources/steammobile.dart'; | import 'package:obtainium/app_sources/steammobile.dart'; | ||||||
| import 'package:obtainium/app_sources/telegramapp.dart'; | import 'package:obtainium/app_sources/telegramapp.dart'; | ||||||
|  | import 'package:obtainium/app_sources/uptodown.dart'; | ||||||
| import 'package:obtainium/app_sources/vlc.dart'; | import 'package:obtainium/app_sources/vlc.dart'; | ||||||
| import 'package:obtainium/components/generated_form.dart'; | import 'package:obtainium/components/generated_form.dart'; | ||||||
| import 'package:obtainium/custom_errors.dart'; | import 'package:obtainium/custom_errors.dart'; | ||||||
| @@ -363,15 +364,23 @@ abstract class AppSource { | |||||||
|     return url; |     return url; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Map<String, String>? get requestHeaders => null; |   Future<Map<String, String>?> getRequestHeaders( | ||||||
|  |       {Map<String, dynamic> additionalSettings = const <String, dynamic>{}, | ||||||
|  |       bool forAPKDownload = false}) async { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<Response> sourceRequest(String url, |   Future<Response> sourceRequest(String url, | ||||||
|       {bool followRedirects = true}) async { |       {bool followRedirects = true, | ||||||
|  |       Map<String, dynamic> additionalSettings = | ||||||
|  |           const <String, dynamic>{}}) async { | ||||||
|  |     var requestHeaders = | ||||||
|  |         await getRequestHeaders(additionalSettings: additionalSettings); | ||||||
|     if (requestHeaders != null || followRedirects == false) { |     if (requestHeaders != null || followRedirects == false) { | ||||||
|       var req = Request('GET', Uri.parse(url)); |       var req = Request('GET', Uri.parse(url)); | ||||||
|       req.followRedirects = followRedirects; |       req.followRedirects = followRedirects; | ||||||
|       if (requestHeaders != null) { |       if (requestHeaders != null) { | ||||||
|         req.headers.addAll(requestHeaders!); |         req.headers.addAll(requestHeaders); | ||||||
|       } |       } | ||||||
|       return Response.fromStream(await Client().send(req)); |       return Response.fromStream(await Client().send(req)); | ||||||
|     } else { |     } else { | ||||||
| @@ -512,6 +521,20 @@ regExValidator(String? value) { | |||||||
|   return null; |   return null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | intValidator(String? value, {bool positive = false}) { | ||||||
|  |   if (value == null) { | ||||||
|  |     return tr('invalidInput'); | ||||||
|  |   } | ||||||
|  |   var num = int.tryParse(value); | ||||||
|  |   if (num == null) { | ||||||
|  |     return tr('invalidInput'); | ||||||
|  |   } | ||||||
|  |   if (positive && num <= 0) { | ||||||
|  |     return tr('invalidInput'); | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  |  | ||||||
| class SourceProvider { | class SourceProvider { | ||||||
|   // Add more source classes here so they are available via the service |   // Add more source classes here so they are available via the service | ||||||
|   List<AppSource> get sources => [ |   List<AppSource> get sources => [ | ||||||
| @@ -519,15 +542,16 @@ class SourceProvider { | |||||||
|         GitLab(), |         GitLab(), | ||||||
|         Codeberg(), |         Codeberg(), | ||||||
|         FDroid(), |         FDroid(), | ||||||
|         IzzyOnDroid(), |  | ||||||
|         FDroidRepo(), |         FDroidRepo(), | ||||||
|         Jenkins(), |         IzzyOnDroid(), | ||||||
|         SourceForge(), |         SourceForge(), | ||||||
|         SourceHut(), |         SourceHut(), | ||||||
|         Aptoide(), |  | ||||||
|         APKMirror(), |  | ||||||
|         APKPure(), |         APKPure(), | ||||||
|  |         Aptoide(), | ||||||
|  |         Uptodown(), | ||||||
|  |         APKMirror(), | ||||||
|         HuaweiAppGallery(), |         HuaweiAppGallery(), | ||||||
|  |         Jenkins(), | ||||||
|         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) |         // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) | ||||||
|         Mullvad(), |         Mullvad(), | ||||||
|         Signal(), |         Signal(), | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -46,10 +46,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: archive |       name: archive | ||||||
|       sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" |       sha256: e0902a06f0e00414e4e3438a084580161279f137aeb862274710f29ec10cf01e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.3.8" |     version: "3.3.9" | ||||||
|   args: |   args: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -198,10 +198,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: dynamic_color |       name: dynamic_color | ||||||
|       sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d |       sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.6.6" |     version: "1.6.7" | ||||||
|   easy_localization: |   easy_localization: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -320,10 +320,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_markdown |       name: flutter_markdown | ||||||
|       sha256: d4a1cb250c4e059586af0235f32e02882860a508e189b61f2b31b8810c1e1330 |       sha256: a10979814c5f4ddbe2b6143fba25d927599e21e3ba65b3862995960606fae78f | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.6.17+2" |     version: "0.6.17+3" | ||||||
|   flutter_plugin_android_lifecycle: |   flutter_plugin_android_lifecycle: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -538,18 +538,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: permission_handler |       name: permission_handler | ||||||
|       sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" |       sha256: ad65ba9af42a3d067203641de3fd9f547ded1410bad3b84400c2b4899faede70 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.4.3" |     version: "11.0.0" | ||||||
|   permission_handler_android: |   permission_handler_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_android |       name: permission_handler_android | ||||||
|       sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3 |       sha256: f23cfe9af0d49c6b9fd8a8b09f7b3301ca7e346204939b5afef4404d36d2608f | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.3.4" |     version: "11.0.1" | ||||||
|   permission_handler_apple: |   permission_handler_apple: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -562,10 +562,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: permission_handler_platform_interface |       name: permission_handler_platform_interface | ||||||
|       sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" |       sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.11.3" |     version: "3.11.5" | ||||||
|   permission_handler_windows: |   permission_handler_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -686,6 +686,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.3.1" |     version: "2.3.1" | ||||||
|  |   shared_storage: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: shared_storage | ||||||
|  |       sha256: "7c65a9d64f0f5521256be974cfd74010af12196657cec9f9fb7b03b2f11bcaf6" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.8.0" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -879,18 +887,18 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_android |       name: webview_flutter_android | ||||||
|       sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa" |       sha256: ddc167c6676f57c8b367d19fcbee267d6dc6adf81bd6c3cb87981d30746e0a6d | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.9.5" |     version: "3.10.1" | ||||||
|   webview_flutter_platform_interface: |   webview_flutter_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: webview_flutter_platform_interface |       name: webview_flutter_platform_interface | ||||||
|       sha256: "9d32a63a5ee111b37482cb3eac3379b9f0992afd27a52ee30279dbf06f41918b" |       sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.5.1" |     version: "2.6.0" | ||||||
|   webview_flutter_wkwebview: |   webview_flutter_wkwebview: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -17,10 +17,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 0.14.10+202 # When changing this, update the tag in main() accordingly | version: 0.14.16+208 # When changing this, update the tag in main() accordingly | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: '>=2.18.2 <3.0.0' |   sdk: '>=3.0.0 <4.0.0' | ||||||
|  |  | ||||||
| # Dependencies specify other packages that your package needs in order to work. | # Dependencies specify other packages that your package needs in order to work. | ||||||
| # To automatically upgrade your package dependencies to the latest versions | # To automatically upgrade your package dependencies to the latest versions | ||||||
| @@ -46,7 +46,7 @@ dependencies: | |||||||
|   html: ^0.15.0 |   html: ^0.15.0 | ||||||
|   shared_preferences: ^2.0.15 |   shared_preferences: ^2.0.15 | ||||||
|   url_launcher: ^6.1.5 |   url_launcher: ^6.1.5 | ||||||
|   permission_handler: ^10.0.0 |   permission_handler: ^11.0.0 | ||||||
|   fluttertoast: ^8.0.9 |   fluttertoast: ^8.0.9 | ||||||
|   device_info_plus: ^9.0.0 |   device_info_plus: ^9.0.0 | ||||||
|   file_picker: ^5.2.10 |   file_picker: ^5.2.10 | ||||||
| @@ -65,6 +65,7 @@ dependencies: | |||||||
|   flutter_archive: ^5.0.0 |   flutter_archive: ^5.0.0 | ||||||
|   hsluv: ^1.1.3 |   hsluv: ^1.1.3 | ||||||
|   connectivity_plus: ^4.0.2 |   connectivity_plus: ^4.0.2 | ||||||
|  |   shared_storage: ^0.8.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user