Compare commits

...

110 Commits

Author SHA1 Message Date
Ushie
795a1c78df build: bump version to v0.0.41 2022-11-24 00:36:03 +03:00
Canny
6ea055d486 feat: use secrets for keys (#528) 2022-11-24 00:27:00 +03:00
Aunali321
b0c4567cb8 feat: use github to get latest patches version if custom sources are used. 2022-11-23 10:03:56 +05:30
Aunali321
6e05120aa5 feat: add chips for patches selection. 2022-11-23 09:48:10 +05:30
Ushie
c5958f1257 docs: add translation field 2022-11-15 14:20:23 +03:00
Aunali321
0faf86c9e9 refactor: restructure codebase. (#501)
* refactor: separate language selection to own widget.

* feat: separate theme changer to own widget.

* refactor: move Appearance UI to separate class.

* refactor: move language selection UI to separate class.

* refactor: move sources selection to separate file.

* refactor: move sources selection to separate file.

* refactor: split settings sections in separate files.

* refactor: move logging section to separate file.

* fix: show toast on bottom.

* fix: recommended patches not being selected by default.

* fix: patch selection selecting non recommended patches.

* fix: experimental toggle not updating.
2022-11-12 21:25:33 +05:30
oSumAtrIX
ee689922a3 feat: do not update on tracing logs 2022-11-11 16:15:23 +01:00
Aunali321
be77a181ec feat: EXPERIMENTAL language switching. 2022-11-10 23:03:07 +05:30
Aunali321
30376c960f build: bump version to v0.0.40 2022-11-10 12:14:50 +05:30
Palm
405147b1c5 fix(i18n): locale capitalization and grammar (#496)
* fix(i18n): locale capitalization and grammar

* fix(i18n): add some more changes requested
2022-11-09 19:07:28 +05:30
Aunali321
d545dfe49b refactor: move exporting patches to another section 2022-11-09 14:20:32 +05:30
Boris M
c571cf2c53 feat: ability to store and load selected patches (#469)
* feat: ability to store and load selected patches

* fix: I18n

* fix: do not append but truncate file

* fix: use json file, minor fixes

* fix: better ui

* WIP

* feat: load patches selection after app selection

* feat: import/export json file

* fix: reformat code

* fix: rare bug on import feature fixed

* fix: move export/ipmort to settings page & import full json

* fix: minor improvements

* fix: minor code quality improvements

* fix: export filename fix

* fix: select list element istead of removing it
2022-11-09 13:06:04 +05:30
Boris M
fd5d71e24d fix: delete cached apk files when picking new one (#481) 2022-11-09 13:05:26 +05:30
Boris M
2c3809d2bc feat: show selected patches count in patches selector view & patch selector card (#466)
* feat: show selected patches count in patches selector view & patch selector card

* fix: add Widget on row

* fix: formatting

* fix: same text style for count
2022-11-09 13:02:39 +05:30
Ushie
0fc8e7cbc8 feat: crowdin workflow 2022-11-05 01:31:56 +03:00
Ushie
787e47f634 Update Crowdin configuration file 2022-11-04 23:14:55 +03:00
nkitsaini
dc47da75f2 feat: Adding support for Exporting APK (#439)
* feat: Adding Export APK support

* Using cr_file_saver to simplify export
2022-11-02 17:22:40 +05:30
Ushie
6b999b0a0c build: bump version to v0.0.39 2022-11-02 00:06:29 +03:00
oSumAtrIX
b00d2d16d4 feat: simplify logging (#305)
Co-authored-by: Ushie <ushiekane@gmail.com>
2022-11-02 00:02:51 +03:00
Boris M
97d4da568b fix: allow tapping on patch card when experimental switch is enabled (#464) 2022-11-02 00:02:33 +03:00
Ushie
e563049f6a build: bump version to v0.0.38 2022-11-01 19:04:23 +03:00
Ushie
cc00d0dc08 bump: kotlin gradle plugin (#461) 2022-11-01 19:03:26 +03:00
Aunali321
2a220c3984 fix: custom sources. 2022-11-01 19:03:30 +05:30
Aunali321
1d440d25be fix: dont select all patches if experimental toggle is off. 2022-11-01 15:36:06 +05:30
Aunali321
ba5234e850 feat: experimental settings to allow patch on any app version. 2022-11-01 15:26:15 +05:30
Ushie
293f7150f1 build: bump version to v0.0.37 2022-10-31 00:59:57 +03:00
Ushie
41b1cec8d3 feat: move to new API domain 2022-10-31 00:57:12 +03:00
Ushie
c129c1eeae build: bump version to v0.0.36
totally didnt forget to bump last night
2022-10-20 10:08:33 +03:00
Ushie
caa9694543 feat: improve explanation of update being unusable 2022-10-20 00:15:48 +03:00
Aunali321
ac79765372 fix: keystore name typo. 2022-10-19 18:55:27 +05:30
Aunali321
39500f054d feat: disable sentry for time being. 2022-10-19 18:53:13 +05:30
Aunali321
f2d5cc91db Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-19 16:33:00 +05:30
Aunali321
84a788fd9e feat: toast for disabled updates. 2022-10-19 16:32:54 +05:30
Ushie
3778bfe1b5 feat(installer): remove restriction of Share APK, closes #410 (#412)
Share APK has a requirement of needing to install the patched app before sharing, this PR removes that as I see no reason for this restriction
2022-10-18 16:16:04 +03:00
Aunali321
63b2d8e0bd build: remove create env step from actions. 2022-10-17 21:51:18 +05:30
Aunali321
e7490b8d75 refactor: disable sentry for now. 2022-10-17 21:39:46 +05:30
Aunali321
2e050d06e8 feat: remove firebase. (#405)
* feat: remove firebase.

* build: remove firebase config.
2022-10-17 20:24:47 +05:30
Aunali321
5fd1154039 build: bump version to 0.0.34 2022-10-17 19:53:46 +05:30
Aunali321
39401a78ec refactor: disable firebase for now. 2022-10-17 19:52:47 +05:30
Aunali321
273aa42b17 refactor: redo the environment system. 2022-10-17 16:48:39 +05:30
Aunali321
603917d21e feat: create env with github actions. 2022-10-17 16:25:22 +05:30
Aunali321
e55cd6a938 feat: add env vars to repo. 2022-10-17 15:06:33 +05:30
Aunali321
2aaed14a3a fix: add firebase options to repo. 2022-10-17 13:59:39 +05:30
Aunali321
511c25163d refactor: delete empty env. 2022-10-17 12:58:26 +05:30
Aunali321
c24e50f3a0 feat: add empty .env config. 2022-10-17 02:09:45 +05:30
Aunali321
2d732288a7 feat: option to delete manager logs. 2022-10-17 01:58:50 +05:30
Aunali321
56e715cd3c build: bump version to v0.0.33 2022-10-17 01:41:11 +05:30
Aunali321
074d8005bc feat: show patch bundle version in patch selector screen. 2022-10-17 01:40:16 +05:30
Aunali321
5b38c9442a fix: increase sleep timer for mount script. 2022-10-17 00:46:48 +05:30
Aunali321
3b8dc66da6 feat: option to delete temporary directory. 2022-10-17 00:41:20 +05:30
Aunali321
f5ebfc92fc feat: decrease time for force refresh of patches. 2022-10-17 00:32:45 +05:30
Aunali321
9de063aced feat: ability to delete keystores. 2022-10-17 00:22:07 +05:30
Aunali321
331691cc9d fix: add env file to repo. 2022-10-16 23:55:09 +05:30
Aunali321
1a97cdf91d refactor: remove useless prints. 2022-10-16 23:54:07 +05:30
Aunali321
fbd4359d61 fix: add missing else block. 2022-10-16 23:43:33 +05:30
Aunali321
f31a60d9bb feat: make firebase crashlytics optional. 2022-10-15 18:29:50 +05:30
Aunali321
79aca0e579 feat: firebase crashlytics for improving manager. 2022-10-15 15:01:31 +05:30
Aunali321
6d35c47b6b feat: make sentry logging optional. 2022-10-15 01:52:10 +05:30
Aunali321
f1261398e9 feat: sentry integration. 2022-10-14 23:35:33 +05:30
Aunali321
007b518503 build: Bump version to v0.0.32 2022-10-12 02:07:23 +05:30
Aunali321
c6edc620c8 feat: cronet lib for non gms device. 2022-10-12 01:52:57 +05:30
Aunali321
3f9d7c9cc0 fix: add back saving patched apps after clearing data. 2022-10-11 20:27:29 +05:30
Aunali321
6c1845e246 feat: add fallback for cronet. 2022-10-11 20:19:50 +05:30
Aunali321
0b2572a730 Bump version to v0.0.31 2022-10-11 02:56:27 +05:30
Aunali321
6fc46d632b feat: make applied patches dialog scrollable. 2022-10-11 02:55:35 +05:30
Aunali321
b2a35813f6 fix: disable update functionality for now. 2022-10-11 02:32:23 +05:30
Aunali321
279b76ad53 fix: don't show previously patched apps after clearing data. 2022-10-11 02:17:13 +05:30
Aunali321
b550016681 fix: use original package name to load patches. 2022-10-10 19:02:42 +05:30
Aunali321
3ab5d12f3e fix: use original package name for changelog. 2022-10-10 18:57:32 +05:30
Aunali321
53bcb8b85f Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-10 18:46:02 +05:30
Aunali321
94397dcb4c feat: store original package name of patches apps. 2022-10-10 18:45:58 +05:30
afn
53fbee2d44 refactor: Reorder sections in readme. 2022-10-10 00:21:49 +05:30
aliernfrog
e46ad3595d fix: permanent keystore path if available (#375) 2022-10-10 00:05:43 +05:30
Aunali321
f2b03b6e69 Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-09 23:01:18 +05:30
Aunali321
0df2a6bdc0 feat: brotli support. 2022-10-09 23:01:10 +05:30
Aunali321
df31e5ccd1 feat: add building from source instructions. 2022-10-09 02:59:50 +05:30
Aunali321
4dfa0dada6 build: bump version to v0.0.30 2022-10-08 22:08:03 +05:30
Aunali321
857a523f84 fix: changelog for youtube. 2022-10-08 22:06:45 +05:30
Aunali321
ceac838706 feat: use new api url. 2022-10-08 19:03:45 +05:30
Aunali321
e8cb6d27fc Merge branch 'flutter' of https://github.com/revanced/revanced-manager into flutter 2022-10-07 18:23:49 +05:30
Aunali321
78428f6bd3 feat: HTTP/3 Support. 2022-10-07 18:23:43 +05:30
Ushie
da94dfba70 build: Android Studio project build config 2022-10-06 12:40:25 +03:00
oSumAtrIX
8275792f45 chore: bump patcher dependency version to v6.0.0 (#355) 2022-10-06 11:26:20 +02:00
j4k0xb
a90923011a feat: allow searching for displayed patch names (#348) 2022-10-03 21:06:12 +03:00
oSumAtrIX
1aa24e2871 fix: print stack trace of patch exceptions (#314) 2022-10-03 22:34:46 +05:30
oSumAtrIX
68ce751745 build: bump version to v0.0.27 2022-10-01 02:15:47 +02:00
oSumAtrIX
74ff64d41a chore: bump patcher dependency version to v5.1.2 2022-10-01 02:15:08 +02:00
Ushie
6d45ccecc2 build: Bump version to v0.0.27 2022-09-30 21:38:15 +03:00
Ushie
5418c36716 feat: use patcher for resourcePatch detection (#337)
Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
2022-09-30 21:36:39 +03:00
Aunali321
ca0657e8f9 feat: decrease cache duration of patches and integrations. 2022-09-28 22:26:54 +05:30
Canny
a5511c2a2c feat: update unsupported button title 2022-09-26 23:36:17 +03:00
Ushie
a346f8857f build: Bump version to v0.0.26 2022-09-26 16:52:04 +03:00
oSumAtrIX
e12532ea4c chore: bump patcher dependency version to v5.1.0 2022-09-26 04:34:23 +02:00
afn
7ecf951bfb fix: tweak card appearances (#296)
* fix: tweak card appearances

* Update patch_selector_card.dart
2022-09-25 18:13:37 +05:30
Aunali321
db18874ea1 fix: now using country code for languages. 2022-09-25 14:38:25 +05:30
oSumAtrIX
18a69776cd feat: en_US.json language asset file 2022-09-25 08:26:08 +02:00
oSumAtrIX
21cadf6450 feat: remove en.json language asset 2022-09-25 08:22:21 +02:00
oSumAtrIX
5ddbe6e252 Update Crowdin configuration file 2022-09-25 08:19:47 +02:00
afn
6d1427e01e fix: move changelog into app item custom card (#294) 2022-09-25 00:19:02 +05:30
Alberto Ponces
6ac901f1d6 build: Bump version to v0.0.24 2022-09-24 13:50:44 +01:00
Alberto Ponces
587ba795bb Revert "fix: Prevent content from being overlapped by system navigation bar"
This reverts commit 4d82ff3011.
2022-09-24 13:49:46 +01:00
Alberto Ponces
6b66c7bbd0 build: Bump version to v0.0.23 2022-09-24 12:16:02 +01:00
Alberto Ponces
f398b6863a Merge branch 'flutter' of github.com:revanced/revanced-manager into flutter 2022-09-24 12:12:24 +01:00
Alberto Ponces
4d82ff3011 fix: Prevent content from being overlapped by system navigation bar 2022-09-24 12:06:36 +01:00
Alberto Ponces
2a0ea78d7f feat: Adds support for patching on x86 and x64 devices
Introduces new aapt2 binaries from revanced/aapt2
2022-09-24 12:05:36 +01:00
Aunali321
4722880647 feat: better patch version background. 2022-09-24 13:47:09 +05:30
Aunali321
32fabcfa3f fix: nav overlapping install buttons. 2022-09-24 12:53:46 +05:30
Alberto Ponces
e0c46e4268 fix: Show "Share log" menu option even if patching has errors 2022-09-23 17:22:39 +01:00
Alberto Ponces
d84230fa22 feat: Merge integrations if a patch or any of its dependencies need them 2022-09-23 17:20:19 +01:00
Alberto Ponces
9561153bfb feat: Hide "Install as Root" button if user does not have root access at all 2022-09-23 15:47:30 +01:00
65 changed files with 2513 additions and 1078 deletions

3
.env Normal file
View File

@@ -0,0 +1,3 @@
sentryDSN=
apiKey=
appId=

45
.github/workflows/crowdin.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Sync Crowdin translations
on:
push:
branches:
- "flutter"
paths:
- "assets/i18n/en_US.json"
- ".github/workflows/crowdin.yml"
schedule:
- cron: "0 0 * * *" # daily
workflow_dispatch:
jobs:
sync-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Crowdin
uses: crowdin/github-action@1.0.4
with:
config: crowdin.yml
upload_translations: true
download_translations: true
push_translations: true
create_pull_request: false
localization_branch_name: i18n_flutter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# commented due to Manager not being ready for the translated files to be in the main branch
# - name: GitHub is so dumb i just cant
# run: |
# sudo chmod -R ugo+rwX .
# - name: Merge
# run: |
# git checkout flutter
# git add *
# git merge i18n_flutter
# git push

View File

@@ -19,6 +19,10 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: 'stable' channel: 'stable'
- name: Set environment variables
run: echo $SECRETS | base64 -d > lib/utils/environment.dart
env:
SECRETS: ${{ secrets.SECRETS }}
- name: Set up Flutter - name: Set up Flutter
run: flutter pub get run: flutter pub get
- name: Generate files with Builder - name: Generate files with Builder

View File

@@ -19,6 +19,10 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: 'stable' channel: 'stable'
- name: Set environment variables
run: echo $SECRETS | base64 -d > lib/utils/environment.dart
env:
SECRETS: ${{ secrets.SECRETS }}
- name: Set up Flutter - name: Set up Flutter
run: flutter pub get run: flutter pub get
- name: Generate files with Builder - name: Generate files with Builder

2
.gitignore vendored
View File

@@ -135,4 +135,4 @@ app.*.map.json
!/dev/ci/**/Gemfile.lock !/dev/ci/**/Gemfile.lock
Firebase related Firebase related
.firebase .firebase

6
.run/main.dart.run.xml Normal file
View File

@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@@ -5,16 +5,31 @@ The official ReVanced Manager based on Flutter.
## 🔽 Download ## 🔽 Download
To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file. To download the Alpha version of Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file.
## 🔴 Issues ## 📝 Prerequisites
For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). 1. Android 8 or higher
2. Does not work on some armv7 devices
## 💭 Discussion 3. [Vanced MicroG](https://github.com/TeamVanced/VancedMicroG/releases) required for YouTube and YouTube Music (Only for non-root)
If you wish to discuss the Manager, a thread has been made under the [#chat](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## ⚠️ Disclaimer ## ⚠️ Disclaimer
*Please note that even though we're releasing the Manager, it is an ALPHA version. Meaning there's a big chance that the Manager might not work at all for you.* *Please note that even though we're releasing the Manager, it is an ALPHA version. There's a big chance that the Manager might not work at all for you.*
## Prerequisites ## 🔴 Issues
1. Android 8 or higher. For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose).
2. Does not work on armv7
3. For YouTube and YouTube Music - Vanced MicroG(Only for non-root). ## 💭 Discussion
If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future.
## 🌐 Translation
[![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced)
If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app)
## 🛠️ Building Manager from source
1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install)
2. Clone the repository locally
3. Add your github token in gradle.properties like [this](https://github.com/revanced/revanced-documentation/wiki/Building-from-source)
4. Open the project in terminal
5. Run `flutter pub get` in terminal
6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull)
7. To build release apk run `flutter build apk`

View File

@@ -9,6 +9,10 @@
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/utils/env_class.g.dart
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml` # section below to disable rules from the `package:flutter_lints/flutter.yaml`

View File

@@ -71,9 +71,14 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ReVanced // ReVanced
implementation "app.revanced:revanced-patcher:5.0.1" implementation "app.revanced:revanced-patcher:6.0.0"
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:7.2.2") implementation("com.android.tools.build:apksig:7.2.2")
// MicroG cronet
implementation("org.microg:cronet-common:$cronetVersion")
implementation("org.microg:cronet-native:$cronetVersion")
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
package app.revanced.manager.flutter package app.revanced.manager.flutter
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.NonNull import androidx.annotation.NonNull
@@ -12,7 +13,7 @@ import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.patch.impl.DexPatchBundle import app.revanced.patcher.util.patch.PatchBundle
import dalvik.system.DexClassLoader import dalvik.system.DexClassLoader
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -42,7 +43,6 @@ class MainActivity : FlutterActivity() {
val selectedPatches = call.argument<List<String>>("selectedPatches") val selectedPatches = call.argument<List<String>>("selectedPatches")
val cacheDirPath = call.argument<String>("cacheDirPath") val cacheDirPath = call.argument<String>("cacheDirPath")
val mergeIntegrations = call.argument<Boolean>("mergeIntegrations") val mergeIntegrations = call.argument<Boolean>("mergeIntegrations")
val resourcePatching = call.argument<Boolean>("resourcePatching")
val keyStoreFilePath = call.argument<String>("keyStoreFilePath") val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
if (patchBundleFilePath != null && if (patchBundleFilePath != null &&
originalFilePath != null && originalFilePath != null &&
@@ -53,7 +53,6 @@ class MainActivity : FlutterActivity() {
selectedPatches != null && selectedPatches != null &&
cacheDirPath != null && cacheDirPath != null &&
mergeIntegrations != null && mergeIntegrations != null &&
resourcePatching != null &&
keyStoreFilePath != null keyStoreFilePath != null
) { ) {
runPatcher( runPatcher(
@@ -67,7 +66,6 @@ class MainActivity : FlutterActivity() {
selectedPatches, selectedPatches,
cacheDirPath, cacheDirPath,
mergeIntegrations, mergeIntegrations,
resourcePatching,
keyStoreFilePath keyStoreFilePath
) )
} else { } else {
@@ -90,7 +88,6 @@ class MainActivity : FlutterActivity() {
selectedPatches: List<String>, selectedPatches: List<String>,
cacheDirPath: String, cacheDirPath: String,
mergeIntegrations: Boolean, mergeIntegrations: Boolean,
resourcePatching: Boolean,
keyStoreFilePath: String keyStoreFilePath: String
) { ) {
val originalFile = File(originalFilePath) val originalFile = File(originalFilePath)
@@ -102,16 +99,20 @@ class MainActivity : FlutterActivity() {
Thread { Thread {
try { try {
val patches = DexPatchBundle( val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
patchBundleFilePath, PatchBundle.Dex(
DexClassLoader(
patchBundleFilePath, patchBundleFilePath,
cacheDirPath, DexClassLoader(
null, patchBundleFilePath,
javaClass.classLoader cacheDirPath,
) null,
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } } javaClass.classLoader
)
).loadPatches().filter { patch -> selectedPatches.any { it == patch.patchName } }
} else {
TODO("VERSION.SDK_INT < CUPCAKE")
}
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
@@ -139,7 +140,6 @@ class MainActivity : FlutterActivity() {
PatcherOptions( PatcherOptions(
inputFile, inputFile,
cacheDirPath, cacheDirPath,
resourcePatching,
Aapt.binary(applicationContext).absolutePath, Aapt.binary(applicationContext).absolutePath,
cacheDirPath, cacheDirPath,
logger = ManagerLogger() logger = ManagerLogger()
@@ -178,9 +178,9 @@ class MainActivity : FlutterActivity() {
} }
patcher.addPatches(patches) patcher.addPatches(patches)
patcher.applyPatches().forEach { (patch, res) -> patcher.executePatches().forEach { (patch, res) ->
if (res.isSuccess) { if (res.isSuccess) {
val msg = "[success] $patch" val msg = "Applied $patch"
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
@@ -193,7 +193,7 @@ class MainActivity : FlutterActivity() {
} }
return@forEach return@forEach
} }
val msg = "[error] $patch:" + res.exceptionOrNull()!! val msg = "$patch failed.\nError:\n" + res.exceptionOrNull()!!.printStackTrace()
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
"update", "update",
@@ -241,7 +241,16 @@ class MainActivity : FlutterActivity() {
) )
) )
} }
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
// Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
try {
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outFile, keyStoreFile)
} catch (e: Exception) {
//log to console
print("Error signing apk: ${e.message}")
e.printStackTrace()
}
handler.post { handler.post {
installerChannel.invokeMethod( installerChannel.invokeMethod(
@@ -299,13 +308,6 @@ class MainActivity : FlutterActivity() {
} }
} }
override fun trace(msg: String) { override fun trace(_msg: String) { /* unused */ }
handler.post {
installerChannel.invokeMethod(
"update",
mapOf("progress" to -1.0, "header" to "", "log" to msg)
)
}
}
} }
} }

View File

@@ -1,5 +1,6 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.cronetVersion = '102.5005.125'
ext.kotlin_version = '1.7.20'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@@ -5,6 +5,7 @@
"disabledLabel": "Disabled", "disabledLabel": "Disabled",
"yesButton": "Yes", "yesButton": "Yes",
"noButton": "No", "noButton": "No",
"warning": "Warning",
"navigationView": { "navigationView": {
"dashboardTab": "Dashboard", "dashboardTab": "Dashboard",
"patcherTab": "Patcher", "patcherTab": "Patcher",
@@ -13,20 +14,22 @@
"homeView": { "homeView": {
"widgetTitle": "Dashboard", "widgetTitle": "Dashboard",
"updatesSubtitle": "Updates", "updatesSubtitle": "Updates",
"patchedSubtitle": "Patched Applications", "patchedSubtitle": "Patched applications",
"updatesAvailable": "Updates available", "updatesAvailable": "Updates available",
"noUpdates": "No updates available", "noUpdates": "No updates available",
"WIP": "Work in progress...",
"noInstallations": "No patched applications installed", "noInstallations": "No patched applications installed",
"installed": "Installed", "installed": "Installed",
"updateDialogTitle": "Update Manager", "updateDialogTitle": "Update Manager",
"updateDialogText": "Are you sure you want to download and update ReVanced Manager?", "updateDialogText": "Are you sure you want to download and update ReVanced Manager?",
"notificationTitle": "ReVanced Manager was updated", "notificationTitle": "Update downloaded",
"notificationText": "Tap to open the app", "notificationText": "Tap to install the update",
"downloadingMessage": "Downloading update...", "downloadingMessage": "Downloading update...",
"installingMessage": "Installing update...", "installingMessage": "Installing update...",
"errorDownloadMessage": "Unable to download update", "errorDownloadMessage": "Unable to download update",
"errorInstallMessage": "Unable to install update", "errorInstallMessage": "Unable to install update",
"noConnection": "No internet connection" "noConnection": "No internet connection",
"updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again."
}, },
"applicationItem": { "applicationItem": {
"patchButton": "Patch", "patchButton": "Patch",
@@ -43,14 +46,13 @@
"patcherView": { "patcherView": {
"widgetTitle": "Patcher", "widgetTitle": "Patcher",
"patchButton": "Patch", "patchButton": "Patch",
"patchDialogTitle": "Warning", "patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?"
"patchDialogText": "You have selected a resource patch and a split APK installation was detected so patching errors may occur.\nAre you sure you want to proceed with patching a split base APK?"
}, },
"appSelectorCard": { "appSelectorCard": {
"widgetTitle": "Select application", "widgetTitle": "Select an application",
"widgetTitleSelected": "Selected application", "widgetTitleSelected": "Selected application",
"widgetSubtitle": "No application selected", "widgetSubtitle": "No application selected",
"noAppsLabel": "No applications found.", "noAppsLabel": "No applications found",
"currentVersion": "Current", "currentVersion": "Current",
"recommendedVersion": "Recommended", "recommendedVersion": "Recommended",
"anyVersion": "any" "anyVersion": "any"
@@ -66,23 +68,26 @@
"widgetSubtitle": "We are online!" "widgetSubtitle": "We are online!"
}, },
"appSelectorView": { "appSelectorView": {
"viewTitle": "Select application", "viewTitle": "Select an application",
"searchBarHint": "Search applications", "searchBarHint": "Search applications",
"storageButton": "Storage", "storageButton": "Storage",
"errorMessage": "Unable to use selected application." "errorMessage": "Unable to use selected application"
}, },
"patchesSelectorView": { "patchesSelectorView": {
"viewTitle": "Select patches", "viewTitle": "Select patches",
"searchBarHint": "Search patches", "searchBarHint": "Search patches",
"doneButton": "Done", "doneButton": "Done",
"recommended": "Recommended",
"all" : "All",
"none" : "None",
"loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app\nPress Done to save current selection",
"noPatchesFound": "No patches found for the selected app", "noPatchesFound": "No patches found for the selected app",
"selectAllPatchesWarningTitle": "Warning",
"selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior." "selectAllPatchesWarningContent": "You are about to select all patches, that includes unrecommended patches and can cause unwanted behavior."
}, },
"patchItem": { "patchItem": {
"unsupportedWarningButton": "Unsupported version", "unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
"unsupportedDialogTitle": "Warning", "unsupportedPatchVersion": "Patch is not supported for this app version. Enable experimental toggle in settings to proceed."
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nCurrent supported versions:\n{supportedVersions}"
}, },
"installerView": { "installerView": {
"widgetTitle": "Installer", "widgetTitle": "Installer",
@@ -93,12 +98,13 @@
"notificationTitle": "ReVanced Manager is patching", "notificationTitle": "ReVanced Manager is patching",
"notificationText": "Tap to return to the installer", "notificationText": "Tap to return to the installer",
"shareApkMenuOption": "Share APK", "shareApkMenuOption": "Share APK",
"exportApkMenuOption": "Export APK",
"shareLogMenuOption": "Share log", "shareLogMenuOption": "Share log",
"installErrorDialogTitle": "Error", "installErrorDialogTitle": "Error",
"installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.", "installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.",
"installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.", "installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.",
"installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.", "installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.",
"noExit": "Installer is still running..." "noExit": "Installer is still running, cannot exit..."
}, },
"settingsView": { "settingsView": {
"widgetTitle": "Settings", "widgetTitle": "Settings",
@@ -106,56 +112,85 @@
"teamSectionTitle": "Team", "teamSectionTitle": "Team",
"infoSectionTitle": "Info", "infoSectionTitle": "Info",
"advancedSectionTitle": "Advanced", "advancedSectionTitle": "Advanced",
"darkThemeLabel": "Dark Mode", "logsSectionTitle": "Logs",
"darkThemeHint": "Welcome to the Dark Side", "darkThemeLabel": "Dark mode",
"darkThemeHint": "Welcome to the dark side",
"dynamicThemeLabel": "Material You", "dynamicThemeLabel": "Material You",
"dynamicThemeHint": "Enjoy an experience closer to your device", "dynamicThemeHint": "Enjoy an experience closer to your device",
"languageLabel": "Language", "languageLabel": "Language",
"englishOption": "English", "englishOption": "English",
"frenchOption": "French",
"sourcesLabel": "Sources", "sourcesLabel": "Sources",
"sourcesLabelHint": "Configure your custom sources", "sourcesLabelHint": "Configure your custom sources",
"orgPatchesLabel" : "Patches Organization", "orgPatchesLabel": "Patches organization",
"sourcesPatchesLabel" : "Patches Source", "sourcesPatchesLabel": "Patches source",
"orgIntegrationsLabel": "Integrations Organization", "orgIntegrationsLabel": "Integrations organization",
"sourcesIntegrationsLabel": "Integrations Source", "sourcesIntegrationsLabel": "Integrations source",
"sourcesResetDialogTitle": "Reset", "sourcesResetDialogTitle": "Reset",
"sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?", "sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?",
"apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?", "apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?",
"contributorsLabel": "Contributors", "contributorsLabel": "Contributors",
"contributorsHint": "A list of contributors of ReVanced", "contributorsHint": "A list of contributors of ReVanced",
"logsLabel": "Logs", "logsLabel": "Logs",
"logsHint": "Share device debug logs", "logsHint": "Share Manager's logs",
"apiURLLabel": "API URL", "apiURLLabel": "API URL",
"apiURLHint": "Configure your custom API URL", "apiURLHint": "Configure your custom API URL",
"selectApiURL": "Select URL", "selectApiURL": "API URL",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
"enabledExperimentalPatches": "Experimental patches support enabled",
"exportSectionTitle": "Import & export",
"aboutLabel": "About", "aboutLabel": "About",
"snackbarMessage": "Copied to clipboard" "snackbarMessage": "Copied to clipboard",
"sentryLabel": "Sentry logging",
"sentryHint": "Send anonymous logs to help us improve ReVanced Manager",
"restartAppForChanges": "Restart the app to apply changes",
"deleteKeystoreLabel": "Delete keystore",
"deleteKeystoreHint": "Delete the keystore used to sign the app",
"deletedKeystore": "Keystore deleted",
"deleteTempDirLabel": "Delete temporary files",
"deleteTempDirHint": "Delete the unused temporary files",
"deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection",
"exportPatchesHint": "Export patches selection to a JSON file",
"exportedPatches": "Patches selection exported",
"noExportFileFound": "No patches selection to export",
"importPatchesLabel": "Import patches selection",
"importPatchesHint": "Import patches selection from a JSON file",
"importedPatches": "Patches selection imported",
"resetStoredPatchesLabel": "Reset patches",
"resetStoredPatchesHint": "Reset the stored patches selection",
"resetStoredPatches": "Patches selection has been reset",
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
"deleteLogsLabel": "Delete logs",
"deleteLogsHint": "Delete collected manager logs",
"deletedLogs": "Logs deleted"
}, },
"appInfoView": { "appInfoView": {
"widgetTitle": "App Info", "widgetTitle": "App info",
"openButton": "Open", "openButton": "Open",
"uninstallButton": "Uninstall", "uninstallButton": "Uninstall",
"patchButton": "Patch", "patchButton": "Patch",
"unpatchButton": "Unpatch", "unpatchButton": "Unpatch",
"unpatchDialogText": "Are you sure you want to unpatch this app?", "unpatchDialogText": "Are you sure you want to unpatch this app?",
"rootDialogTitle": "Error", "rootDialogTitle": "Error",
"rootDialogText": "App was installed with root mode enabled but currently root mode is disabled.\nPlease enable root mode first.", "rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package Name", "packageNameLabel": "Package name",
"installTypeLabel": "Installation Type", "originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type",
"rootTypeLabel": "Root", "rootTypeLabel": "Root",
"nonRootTypeLabel": "Non-root", "nonRootTypeLabel": "Non-root",
"patchedDateLabel": "Patched Date", "patchedDateLabel": "Patched date",
"patchedDateHint": "{date} at {time}", "patchedDateHint": "{date} at {time}",
"appliedPatchesLabel": "Applied Patches", "appliedPatchesLabel": "Applied patches",
"appliedPatchesHint": "{quantity} applied patches" "appliedPatchesHint": "{quantity} applied patches",
"updateNotImplemented": "This feature has not been implemented yet"
}, },
"contributorsView": { "contributorsView": {
"widgetTitle": "Contributors", "widgetTitle": "Contributors",
"patcherContributors": "Patcher Contributors", "patcherContributors": "Patcher contributors",
"patchesContributors": "Patches Contributors", "patchesContributors": "Patches contributors",
"integrationsContributors": "Integrations Contributors", "integrationsContributors": "Integrations contributors",
"cliContributors": "CLI Contributors", "cliContributors": "CLI contributors",
"managerContributors": "Manager Contributors" "managerContributors": "Manager contributors"
} }
} }

197
assets/i18n/hi_IN.json Normal file
View File

@@ -0,0 +1,197 @@
{
"okButton": "ठीक है",
"cancelButton": "रद्द करें",
"enabledLabel": "सक्रिय",
"disabledLabel": "निष्क्रिय",
"yesButton": "हाँ",
"noButton": "नहीं",
"navigationView": {
"dashboardTab": "नियंत्रण-पट्ट",
"patcherTab": "पैचर",
"settingsTab": "सेटिंग्स"
},
"homeView": {
"widgetTitle": "नियंत्रण पट्ट",
"updatesSubtitle": "अपडेट",
"patchedSubtitle": "Patched applications",
"updatesAvailable": "अपडेट उपलब्ध है",
"noUpdates": "कोई अपडेट उपलब्ध नहीं",
"WIP": "Work in progress...",
"noInstallations": "कोई पैबंद किये हुआ अनुप्रयोग नहीं है।",
"installed": "इंस्टॉल किया हुआ",
"updateDialogTitle": "अपडेट Manager",
"updateDialogText": "क्या आप ReVanced Manager को डाउनलोड और अपडेट करना चाहते है?",
"notificationTitle": "Update downloaded",
"notificationText": "Tap to install the update",
"downloadingMessage": "अपडेट डाउनलोड हो रहा है",
"installingMessage": "अपडेट इंस्टॉल हो रहा है",
"errorDownloadMessage": "अपडेट डाउनलोड करने मे असफल",
"errorInstallMessage": "अपडेट इंस्टॉल करने में असफल",
"noConnection": "कोई इंटरनेट कनेक्शन नहीं",
"updatesDisabled": "पैच किए गए ऐप को अपडेट करना वर्तमान में अक्षम है। ऐप को फिर से रीपैच करें।"
},
"applicationItem": {
"patchButton": "पैबंद",
"infoButton": "जानकारी",
"changelogLabel": "परिवर्तन पत्र"
},
"latestCommitCard": {
"loadingLabel": "लोड हो रहा है...",
"timeagoLabel": "{time} पहले",
"patcherLabel": "पैबंद: ",
"managerLabel": "Manager: ",
"updateButton": "Manager अपडेट करे"
},
"patcherView": {
"widgetTitle": "पैचर",
"patchButton": "पैबंद",
"patchDialogTitle": "चेतावनी",
"patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?"
},
"appSelectorCard": {
"widgetTitle": "Select an application",
"widgetTitleSelected": "चुना हुआ ऐप्लकैशन",
"widgetSubtitle": "कोई ऐप्लकैशन चुना हुआ नहीं",
"noAppsLabel": "No applications found",
"currentVersion": "वर्तमान",
"recommendedVersion": "अनुशंसित",
"anyVersion": "कोई"
},
"patchSelectorCard": {
"widgetTitle": "पैच चुने",
"widgetTitleSelected": "चुने हुए पैच",
"widgetSubtitle": "पहले किसी एप्लिकेशन को चुने",
"widgetEmptySubtitle": "कोई पैच चुना हुआ नहीं"
},
"socialMediaCard": {
"widgetTitle": "सामाजिक",
"widgetSubtitle": "हम ऑनलाइन है"
},
"appSelectorView": {
"viewTitle": "Select an application",
"searchBarHint": "ऐप्लकैशन खोजे",
"storageButton": "स्टोरेज",
"errorMessage": "Unable to use selected application"
},
"patchesSelectorView": {
"viewTitle": "पैच चुने",
"searchBarHint": "पैच खोजे",
"doneButton": "पूर्ण",
"loadPatchesSelection": "Load patches selection",
"noSavedPatches": "No saved patches for the selected app\nPress Done to save current selection",
"noPatchesFound": "चुने हुए ऐप्लकैशन के लिए कोई पैच नहीं मिले",
"selectAllPatchesWarningTitle": "चेतावनी",
"selectAllPatchesWarningContent": "आप सभी पैच का चयन करने वाले हैं, जिसमें अनुशंसित पैच शामिल हैं और अवांछित व्यवहार का कारण बन सकते हैं।"
},
"patchItem": {
"unsupportedWarningButton": "चेतावनी",
"unsupportedDialogTitle": "चेतावनी",
"unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}",
"unsupportedPatchVersion": "इस ऐप संस्करण के लिए पैच समर्थित नहीं है। आगे बढ़ने के लिए सेटिंग में प्रयोगात्मक टॉगल सक्षम करें."
},
"installerView": {
"widgetTitle": "इंस्टॉल कर्ता",
"installButton": "इंस्टॉल करे",
"installRootButton": "रूट के रूप मे इंस्टॉल करे",
"openButton": "खोलें",
"shareButton": "फाइल शेयर करे",
"notificationTitle": "ReVanced Manager पैच कर रहा है",
"notificationText": "इंस्टॉल कर्ता पर जाने के लिए टैप करे",
"shareApkMenuOption": "APK शेयर करे",
"exportApkMenuOption": "निर्यात APK",
"shareLogMenuOption": "लॉग शेयर करें",
"installErrorDialogTitle": "त्रुटि",
"installErrorDialogText1": "वर्तमान पैच चयन के साथ रूट इंस्टॉल संभव नहीं है।\nअपने ऐप को रीपैच करें या नॉन-रूट इंस्टॉल चुनें।",
"installErrorDialogText2": "वर्तमान पैच चयन के साथ नॉन-रूट इंस्टॉल संभव नहीं है।\nयदि आपने अपना डिवाइस रूट किया है तो अपने ऐप को रीपैच करें या रूट इंस्टॉल चुनें।",
"installErrorDialogText3": "रूट इंस्टॉल संभव नहीं है क्योंकि मूल APK को स्टोरेज से चुना गया था।\nएक इंस्टॉल किया गया ऐप चुनें या नॉन-रूट इंस्टॉल चुनें।",
"noExit": "Installer is still running, cannot exit..."
},
"settingsView": {
"widgetTitle": "सेटिंग्स",
"appearanceSectionTitle": "स्वरूप",
"teamSectionTitle": "टीम",
"infoSectionTitle": "जानकारी",
"advancedSectionTitle": "एडवांसड",
"logsSectionTitle": "लॉग्स",
"darkThemeLabel": "Dark mode",
"darkThemeHint": "Welcome to the dark side",
"dynamicThemeLabel": "मेटीरियल यू",
"dynamicThemeHint": "अपने डिवाइस के करीब एक अनुभव का आनंद लें",
"languageLabel": "भाषा",
"englishOption": "अंग्रेज़ी",
"frenchOption": "फ्रेंच",
"sourcesLabel": "स्रोत",
"sourcesLabelHint": "अपने कस्टम साधन कॉन्फ़िगर करे",
"orgPatchesLabel": "Patches organization",
"sourcesPatchesLabel": "Patches source",
"orgIntegrationsLabel": "Integrations organization",
"sourcesIntegrationsLabel": "Integrations source",
"sourcesResetDialogTitle": "रीसेट करें",
"sourcesResetDialogText": "क्या आप वाकई कस्टम साधन को डिफ़ॉल्ट वैल्यू पर रीसेट करना चाहते हैं?",
"apiURLResetDialogText": "क्या आप वाकई कस्टम API URL को डिफ़ॉल्ट वैल्यू पर रीसेट करना चाहते हैं?",
"contributorsLabel": "योगदानकर्ता",
"contributorsHint": "ReVanced के योगदानकर्ताओ की सूची",
"logsLabel": "लॉग्स",
"logsHint": "Share Manager's logs",
"apiURLLabel": "API URL",
"apiURLHint": "अपनी कस्टम API URL कॉन्फ़िगर करे",
"selectApiURL": "API URL",
"experimentalPatchesLabel": "Experimental patches support",
"experimentalPatchesHint": "Enable usage of unsupported patches in any app version",
"enabledExperimentalPatches": "Experimental patches support enabled",
"exportSectionTitle": "Import & export",
"aboutLabel": "विवरण",
"snackbarMessage": "क्लिपबोर्ड में कॉपी हो गया है",
"sentryLabel": "Sentry logging",
"sentryHint": "उन्नत प्रबंधक को बेहतर बनाने में हमारी सहायता करने के लिए अनाम लॉग भेजें",
"restartAppForChanges": "Restart the app to apply changes",
"deleteKeystoreLabel": "कीस्टोर मिटाएं",
"deleteKeystoreHint": "ऐप पर हस्ताक्षर करने के लिए उपयोग की जाने वाली कीस्टोर हटाएं",
"deletedKeystore": "कीस्टोर हटा दिया गया",
"deleteTempDirLabel": "Delete temporary files",
"deleteTempDirHint": "Delete the unused temporary files",
"deletedTempDir": "Temporary files deleted",
"exportPatchesLabel": "Export patches selection",
"exportPatchesHint": "Export patches selection to a JSON file",
"exportedPatches": "Patches selection exported",
"noExportFileFound": "No patches selection to export",
"importPatchesLabel": "Import patches selection",
"importPatchesHint": "Import patches selection from a JSON file",
"importedPatches": "Patches selection imported",
"resetStoredPatchesLabel": "Reset patches",
"resetStoredPatchesHint": "Reset the stored patches selection",
"resetStoredPatches": "Patches selection has been reset",
"jsonSelectorErrorMessage": "Unable to use selected JSON file",
"deleteLogsLabel": "लॉग हटाएं",
"deleteLogsHint": "एकत्रित प्रबंधक लॉग हटाएं",
"deletedLogs": "लॉग हटा दिए गए"
},
"appInfoView": {
"widgetTitle": "App info",
"openButton": "खोलें",
"uninstallButton": "अनइंस्टॉल करें",
"patchButton": "पैच",
"unpatchButton": "अनपैच करे",
"unpatchDialogText": "क्या वाकई आप इस एप को अनपैच करना चाहते हैं?",
"rootDialogTitle": "त्रुटि",
"rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.",
"packageNameLabel": "Package name",
"originalPackageNameLabel": "Original package name",
"installTypeLabel": "Installation type",
"rootTypeLabel": "रूट",
"nonRootTypeLabel": "नॉन-रूट",
"patchedDateLabel": "Patched date",
"patchedDateHint": "{date} {time} पर",
"appliedPatchesLabel": "Applied patches",
"appliedPatchesHint": "{quantity} लागू किए हुआ पैच",
"updateNotImplemented": "This feature has not been implemented yet"
},
"contributorsView": {
"widgetTitle": "योगदानकर्ता",
"patcherContributors": "Patcher contributors",
"patchesContributors": "Patches contributors",
"integrationsContributors": "Integrations contributors",
"cliContributors": "CLI contributors",
"managerContributors": "Manager contributors"
}
}

View File

@@ -1,3 +1,9 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
commit_message: 'chore(i18n): sync translations'
preserve_hierarchy: true
files: files:
- source: /assets/i18n/en.json - source: assets/i18n/en_US.json
translation: /assets/i18n/%two_letters_code%.json translation: assets/i18n/%locale_with_underscore%.json

View File

@@ -1,3 +1,4 @@
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
@@ -37,6 +38,7 @@ import 'package:stacked_services/stacked_services.dart';
LazySingleton(classType: PatcherAPI), LazySingleton(classType: PatcherAPI),
LazySingleton(classType: RevancedAPI), LazySingleton(classType: RevancedAPI),
LazySingleton(classType: GithubAPI), LazySingleton(classType: GithubAPI),
LazySingleton(classType: CrowdinAPI),
LazySingleton(classType: Toast), LazySingleton(classType: Toast),
], ],
) )

View File

@@ -2,15 +2,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart';
import 'package:revanced_manager/utils/environment.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked_themes/stacked_themes.dart'; import 'package:stacked_themes/stacked_themes.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/data/latest.dart' as tz;
late SharedPreferences prefs;
Future main() async { Future main() async {
await ThemeManager.initialise(); await ThemeManager.initialise();
await setupLocator(); await setupLocator();
@@ -18,9 +23,35 @@ Future main() async {
await locator<ManagerAPI>().initialize(); await locator<ManagerAPI>().initialize();
String apiUrl = locator<ManagerAPI>().getApiUrl(); String apiUrl = locator<ManagerAPI>().getApiUrl();
await locator<RevancedAPI>().initialize(apiUrl); await locator<RevancedAPI>().initialize(apiUrl);
await locator<CrowdinAPI>().initialize();
bool isSentryEnabled = locator<ManagerAPI>().isSentryEnabled();
locator<GithubAPI>().initialize(); locator<GithubAPI>().initialize();
await locator<PatcherAPI>().initialize(); await locator<PatcherAPI>().initialize();
tz.initializeTimeZones(); tz.initializeTimeZones();
prefs = await SharedPreferences.getInstance();
await SentryFlutter.init(
(options) {
options
..dsn = isSentryEnabled ? Environment.sentryDSN : ''
..environment = 'alpha'
..release = '0.1'
..tracesSampleRate = 1.0
..anrEnabled = true
..enableOutOfMemoryTracking = true
..sampleRate = isSentryEnabled ? 1.0 : 0.0
..beforeSend = (event, hint) {
if (isSentryEnabled) {
return event;
} else {
return null;
}
} as BeforeSendCallback?;
},
appRunner: () {
runApp(const MyApp());
},
);
runApp(const MyApp()); runApp(const MyApp());
} }
@@ -29,15 +60,25 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String rawLocale = prefs.getString('language') ?? 'en_US';
String replaceLocale = rawLocale.replaceAll('_', '-');
List<String> localeList = replaceLocale.split('-');
Locale locale = Locale(localeList[0], localeList[1]);
return DynamicThemeBuilder( return DynamicThemeBuilder(
title: 'ReVanced Manager', title: 'ReVanced Manager',
home: const NavigationView(), home: const NavigationView(),
localizationsDelegates: [ localizationsDelegates: [
FlutterI18nDelegate( FlutterI18nDelegate(
translationLoader: FileTranslationLoader( translationLoader: FileTranslationLoader(
fallbackFile: 'en', forcedLocale: locale,
basePath: 'assets/i18n', basePath: 'assets/i18n',
useCountryCode: true,
), ),
missingTranslationHandler: (key, locale) {
print(
'--> Missing translation: key: $key, languageCode: ${locale?.languageCode}');
},
), ),
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate

View File

@@ -8,6 +8,7 @@ part 'patched_application.g.dart';
class PatchedApplication { class PatchedApplication {
String name; String name;
String packageName; String packageName;
String originalPackageName;
String version; String version;
final String apkFilePath; final String apkFilePath;
@JsonKey( @JsonKey(
@@ -25,6 +26,7 @@ class PatchedApplication {
PatchedApplication({ PatchedApplication({
required this.name, required this.name,
required this.packageName, required this.packageName,
required this.originalPackageName,
required this.version, required this.version,
required this.apkFilePath, required this.apkFilePath,
required this.icon, required this.icon,

View File

@@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:injectable/injectable.dart' hide Environment;
import 'package:revanced_manager/utils/environment.dart';
import 'package:sentry_dio/sentry_dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@lazySingleton
class CrowdinAPI {
late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final apiKey = Environment.crowdinKEY;
Future<void> initialize() async {
try {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.crowdin.com/api/v2',
));
_dio.interceptors.add(_dioCacheManager.interceptor);
_dio.addSentry(
captureFailedRequests: true,
);
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
}
Future<void> clearAllCache() async {
try {
await _dioCacheManager.clearAll();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
}
Future<List> getLanguages() async {
try {
var response = await _dio.get(
'/projects',
options: buildCacheOptions(
const Duration(hours: 6),
maxStale: const Duration(days: 1),
options: Options(
headers: {
'Authorization': 'Bearer $apiKey',
},
contentType: 'application/json',
),
),
);
List targetLanguages =
await response.data['data'][0]['data']['targetLanguages'];
return targetLanguages;
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return [];
}
}
}

View File

@@ -6,22 +6,16 @@ import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_dio/sentry_dio.dart';
@lazySingleton @lazySingleton
class GithubAPI { class GithubAPI {
final Dio _dio = Dio( late Dio _dio = Dio();
BaseOptions(baseUrl: 'https://api.github.com'),
)..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: 10000,
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions( final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1), const Duration(hours: 6),
maxStale: const Duration(days: 7), maxStale: const Duration(days: 1),
); );
final Map<String, String> repoAppPath = { final Map<String, String> repoAppPath = {
'com.google.android.youtube': 'youtube', 'com.google.android.youtube': 'youtube',
@@ -31,14 +25,30 @@ class GithubAPI {
'com.zhiliaoapp.musically': 'tiktok', 'com.zhiliaoapp.musically': 'tiktok',
'de.dwd.warnapp': 'warnwetter', 'de.dwd.warnapp': 'warnwetter',
'com.garzotto.pflotsh.ecmwf_a': 'ecmwf', 'com.garzotto.pflotsh.ecmwf_a': 'ecmwf',
'com.spotify.music': 'spotify',
}; };
void initialize() { void initialize() async {
_dio.interceptors.add(_dioCacheManager.interceptor); try {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.github.com',
));
_dio.interceptors.add(_dioCacheManager.interceptor);
_dio.addSentry(
captureFailedRequests: true,
);
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
await _dioCacheManager.clearAll(); try {
await _dioCacheManager.clearAll();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
Future<Map<String, dynamic>?> _getLatestRelease(String repoName) async { Future<Map<String, dynamic>?> _getLatestRelease(String repoName) async {
@@ -48,7 +58,8 @@ class GithubAPI {
options: _cacheOptions, options: _cacheOptions,
); );
return response.data; return response.data;
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
} }
@@ -65,17 +76,21 @@ class GithubAPI {
'/repos/$repoName/commits', '/repos/$repoName/commits',
queryParameters: { queryParameters: {
'path': path, 'path': path,
'per_page': 3,
'since': since.toIso8601String(), 'since': since.toIso8601String(),
}, },
options: _cacheOptions, options: _cacheOptions,
); );
List<dynamic> commits = response.data; List<dynamic> commits = response.data;
return commits return commits
.map((commit) => .map(
(commit['commit']['message'] as String).split('\n')[0]) (commit) => (commit['commit']['message']).split('\n')[0] +
' - ' +
commit['commit']['author']['name'] +
'\n' as String,
)
.toList(); .toList();
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return List.empty(); return List.empty();
} }
} }
@@ -94,7 +109,8 @@ class GithubAPI {
); );
} }
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
return null; return null;
@@ -108,9 +124,24 @@ class GithubAPI {
List<dynamic> list = jsonDecode(f.readAsStringSync()); List<dynamic> list = jsonDecode(f.readAsStringSync());
patches = list.map((patch) => Patch.fromJson(patch)).toList(); patches = list.map((patch) => Patch.fromJson(patch)).toList();
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return List.empty(); return List.empty();
} }
return patches; return patches;
} }
Future<String> getLastestReleaseVersion(String repoName) async {
try {
Map<String, dynamic>? release = await _getLatestRelease(repoName);
if (release != null) {
return release['tag_name'];
} else {
return 'Unknown';
}
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return '';
}
}
} }

View File

@@ -3,12 +3,14 @@ import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/services/revanced_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@lazySingleton @lazySingleton
@@ -18,8 +20,9 @@ class ManagerAPI {
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
final String patcherRepo = 'revanced-patcher'; final String patcherRepo = 'revanced-patcher';
final String cliRepo = 'revanced-cli'; final String cliRepo = 'revanced-cli';
late String storedPatchesFile = '/selected-patches.json';
late SharedPreferences _prefs; late SharedPreferences _prefs;
String defaultApiUrl = 'https://revanced-releases-api.afterst0rm.xyz'; String defaultApiUrl = 'https://releases.revanced.app/';
String defaultPatcherRepo = 'revanced/revanced-patcher'; String defaultPatcherRepo = 'revanced/revanced-patcher';
String defaultPatchesRepo = 'revanced/revanced-patches'; String defaultPatchesRepo = 'revanced/revanced-patches';
String defaultIntegrationsRepo = 'revanced/revanced-integrations'; String defaultIntegrationsRepo = 'revanced/revanced-integrations';
@@ -28,6 +31,8 @@ class ManagerAPI {
Future<void> initialize() async { Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
storedPatchesFile =
(await getApplicationDocumentsDirectory()).path + storedPatchesFile;
} }
String getApiUrl() { String getApiUrl() {
@@ -81,6 +86,37 @@ class ManagerAPI {
await _prefs.setBool('useDarkTheme', value); await _prefs.setBool('useDarkTheme', value);
} }
bool isSentryEnabled() {
return _prefs.getBool('sentryEnabled') ?? true;
}
Future<void> setSentryStatus(bool value) async {
await _prefs.setBool('sentryEnabled', value);
}
bool areExperimentalPatchesEnabled() {
return _prefs.getBool('experimentalPatchesEnabled') ?? false;
}
Future<void> enableExperimentalPatchesStatus(bool value) async {
await _prefs.setBool('experimentalPatchesEnabled', value);
}
Future<void> deleteTempFolder() async {
final Directory dir = Directory('/data/local/tmp/revanced-manager');
if (await dir.exists()) {
await dir.delete(recursive: true);
}
}
Future<void> deleteKeystore() async {
final File keystore = File(
'/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore');
if (await keystore.exists()) {
await keystore.delete();
}
}
List<PatchedApplication> getPatchedApps() { List<PatchedApplication> getPatchedApps() {
List<String> apps = _prefs.getStringList('patchedApps') ?? []; List<String> apps = _prefs.getStringList('patchedApps') ?? [];
return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList();
@@ -116,9 +152,13 @@ class ManagerAPI {
await setPatchedApps(patchedApps); await setPatchedApps(patchedApps);
} }
void clearAllData() { void clearAllData() async {
_revancedAPI.clearAllCache(); try {
_githubAPI.clearAllCache(); _revancedAPI.clearAllCache();
_githubAPI.clearAllCache();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
Future<Map<String, List<dynamic>>> getContributors() async { Future<Map<String, List<dynamic>>> getContributors() async {
@@ -126,35 +166,50 @@ class ManagerAPI {
} }
Future<List<Patch>> getPatches() async { Future<List<Patch>> getPatches() async {
String repoName = getPatchesRepo(); try {
if (repoName == defaultPatchesRepo) { String repoName = getPatchesRepo();
return await _revancedAPI.getPatches(); if (repoName == defaultPatchesRepo) {
} else { return await _revancedAPI.getPatches();
return await _githubAPI.getPatches(repoName); } else {
return await _githubAPI.getPatches(repoName);
}
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return [];
} }
} }
Future<File?> downloadPatches() async { Future<File?> downloadPatches() async {
String repoName = getPatchesRepo(); try {
if (repoName == defaultPatchesRepo) { String repoName = getPatchesRepo();
return await _revancedAPI.getLatestReleaseFile( if (repoName == defaultPatchesRepo) {
'.jar', return await _revancedAPI.getLatestReleaseFile(
defaultPatchesRepo, '.jar',
); defaultPatchesRepo,
} else { );
return await _githubAPI.getLatestReleaseFile('.jar', repoName); } else {
return await _githubAPI.getLatestReleaseFile('.jar', repoName);
}
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null;
} }
} }
Future<File?> downloadIntegrations() async { Future<File?> downloadIntegrations() async {
String repoName = getIntegrationsRepo(); try {
if (repoName == defaultIntegrationsRepo) { String repoName = getIntegrationsRepo();
return await _revancedAPI.getLatestReleaseFile( if (repoName == defaultIntegrationsRepo) {
'.apk', return await _revancedAPI.getLatestReleaseFile(
defaultIntegrationsRepo, '.apk',
); defaultIntegrationsRepo,
} else { );
return await _githubAPI.getLatestReleaseFile('.apk', repoName); } else {
return await _githubAPI.getLatestReleaseFile('.apk', repoName);
}
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null;
} }
} }
@@ -177,6 +232,13 @@ class ManagerAPI {
); );
} }
Future<String?> getLatestPatchesVersion() async {
return await _revancedAPI.getLatestReleaseVersion(
'.json',
defaultPatchesRepo,
);
}
Future<String> getCurrentManagerVersion() async { Future<String> getCurrentManagerVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
return packageInfo.version; return packageInfo.version;
@@ -213,6 +275,7 @@ class ManagerAPI {
PatchedApplication( PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
@@ -241,6 +304,7 @@ class ManagerAPI {
PatchedApplication( PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
@@ -261,8 +325,10 @@ class ManagerAPI {
List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps); List<PatchedApplication> toRemove = await getAppsToRemove(patchedApps);
patchedApps.removeWhere((a) => toRemove.contains(a)); patchedApps.removeWhere((a) => toRemove.contains(a));
for (PatchedApplication app in patchedApps) { for (PatchedApplication app in patchedApps) {
app.hasUpdates = await hasAppUpdates(app.packageName, app.patchDate); app.hasUpdates =
app.changelog = await getAppChangelog(app.packageName, app.patchDate); await hasAppUpdates(app.originalPackageName, app.patchDate);
app.changelog =
await getAppChangelog(app.originalPackageName, app.patchDate);
if (!app.hasUpdates) { if (!app.hasUpdates) {
String? currentInstalledVersion = String? currentInstalledVersion =
(await DeviceApps.getApp(app.packageName))?.versionName; (await DeviceApps.getApp(app.packageName))?.versionName;
@@ -314,7 +380,7 @@ class ManagerAPI {
newCommits = await _githubAPI.getCommits( newCommits = await _githubAPI.getCommits(
packageName, packageName,
getPatchesRepo(), getPatchesRepo(),
DateTime(2022, 3, 20, 21, 06, 01), patchDate,
); );
} }
return newCommits; return newCommits;
@@ -329,4 +395,43 @@ class ManagerAPI {
} }
return app != null && app.isSplit; return app != null && app.isSplit;
} }
Future<void> setSelectedPatches(String app, List<String> patches) async {
final File selectedPatchesFile = File(storedPatchesFile);
Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
if (patches.isEmpty) {
patchesMap.remove(app);
} else {
patchesMap[app] = patches;
}
if (selectedPatchesFile.existsSync()) {
selectedPatchesFile.createSync(recursive: true);
}
selectedPatchesFile.writeAsString(jsonEncode(patchesMap));
}
Future<List<String>> getSelectedPatches(String app) async {
Map<String, dynamic> patchesMap = await readSelectedPatchesFile();
if (patchesMap.isNotEmpty) {
final List<String> patches =
List.from(patchesMap.putIfAbsent(app, () => List.empty()));
return patches;
}
return List.empty();
}
Future<Map<String, dynamic>> readSelectedPatchesFile() async {
final File selectedPatchesFile = File(storedPatchesFile);
if (selectedPatchesFile.existsSync()) {
String string = selectedPatchesFile.readAsStringSync();
if (string.trim().isEmpty) return {};
return json.decode(string);
}
return {};
}
Future<void> resetLastSelectedPatches() async {
final File selectedPatchesFile = File(storedPatchesFile);
selectedPatchesFile.deleteSync();
}
} }

View File

@@ -10,7 +10,9 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:share_extend/share_extend.dart'; import 'package:share_extend/share_extend.dart';
import 'package:cr_file_saver/file_saver.dart';
@lazySingleton @lazySingleton
class PatcherAPI { class PatcherAPI {
@@ -18,6 +20,7 @@ class PatcherAPI {
MethodChannel('app.revanced.manager.flutter/patcher'); MethodChannel('app.revanced.manager.flutter/patcher');
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
late Directory _dataDir;
late Directory _tmpDir; late Directory _tmpDir;
late File _keyStoreFile; late File _keyStoreFile;
List<Patch> _patches = []; List<Patch> _patches = [];
@@ -26,8 +29,9 @@ class PatcherAPI {
Future<void> initialize() async { Future<void> initialize() async {
await _loadPatches(); await _loadPatches();
Directory appCache = await getTemporaryDirectory(); Directory appCache = await getTemporaryDirectory();
_dataDir = await getExternalStorageDirectory() ?? appCache;
_tmpDir = Directory('${appCache.path}/patcher'); _tmpDir = Directory('${appCache.path}/patcher');
_keyStoreFile = File('${appCache.path}/revanced-manager.keystore'); _keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore');
cleanPatcher(); cleanPatcher();
} }
@@ -42,7 +46,8 @@ class PatcherAPI {
if (_patches.isEmpty) { if (_patches.isEmpty) {
_patches = await _managerAPI.getPatches(); _patches = await _managerAPI.getPatches();
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
_patches = List.empty(); _patches = List.empty();
} }
} }
@@ -61,7 +66,8 @@ class PatcherAPI {
filteredApps.add(app); filteredApps.add(app);
} }
} }
} catch (e) { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
continue; continue;
} }
} }
@@ -70,14 +76,10 @@ class PatcherAPI {
} }
Future<List<Patch>> getFilteredPatches(String packageName) async { Future<List<Patch>> getFilteredPatches(String packageName) async {
String newPackageName = packageName.replaceFirst(
'app.revanced.',
'com.google.',
);
return _patches return _patches
.where((patch) => .where((patch) =>
!patch.name.contains('settings') && !patch.name.contains('settings') &&
patch.compatiblePackages.any((pack) => pack.name == newPackageName)) patch.compatiblePackages.any((pack) => pack.name == packageName))
.toList(); .toList();
} }
@@ -87,10 +89,21 @@ class PatcherAPI {
.toList(); .toList();
} }
bool dependencyNeedsIntegrations(String name) {
return name.contains('integrations') ||
_patches.any(
(patch) =>
patch.name == name &&
(patch.dependencies.any(
(dep) => dependencyNeedsIntegrations(dep),
)),
);
}
Future<bool> needsIntegrations(List<Patch> selectedPatches) async { Future<bool> needsIntegrations(List<Patch> selectedPatches) async {
return selectedPatches.any( return selectedPatches.any(
(patch) => patch.dependencies.any( (patch) => patch.dependencies.any(
(dep) => dep.contains('integrations'), (dep) => dependencyNeedsIntegrations(dep),
), ),
); );
} }
@@ -115,14 +128,19 @@ class PatcherAPI {
String packageName, String packageName,
String originalFilePath, String originalFilePath,
) async { ) async {
bool hasRootPermissions = await _rootAPI.hasRootPermissions(); try {
if (hasRootPermissions) { bool hasRootPermissions = await _rootAPI.hasRootPermissions();
originalFilePath = await _rootAPI.getOriginalFilePath( if (hasRootPermissions) {
packageName, originalFilePath = await _rootAPI.getOriginalFilePath(
originalFilePath, packageName,
); originalFilePath,
);
}
return originalFilePath;
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return originalFilePath;
} }
return originalFilePath;
} }
Future<void> runPatcher( Future<void> runPatcher(
@@ -131,7 +149,6 @@ class PatcherAPI {
List<Patch> selectedPatches, List<Patch> selectedPatches,
) async { ) async {
bool mergeIntegrations = await needsIntegrations(selectedPatches); bool mergeIntegrations = await needsIntegrations(selectedPatches);
bool resourcePatching = await needsResourcePatching(selectedPatches);
bool includeSettings = await needsSettingsPatch(selectedPatches); bool includeSettings = await needsSettingsPatch(selectedPatches);
if (includeSettings) { if (includeSettings) {
try { try {
@@ -143,7 +160,8 @@ class PatcherAPI {
if (settingsPatch != null) { if (settingsPatch != null) {
selectedPatches.add(settingsPatch); selectedPatches.add(settingsPatch);
} }
} catch (e) { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
// ignore // ignore
} }
} }
@@ -153,6 +171,7 @@ class PatcherAPI {
integrationsFile = await _managerAPI.downloadIntegrations(); integrationsFile = await _managerAPI.downloadIntegrations();
} }
if (patchBundleFile != null) { if (patchBundleFile != null) {
_dataDir.createSync();
_tmpDir.createSync(); _tmpDir.createSync();
Directory workDir = _tmpDir.createTempSync('tmp-'); Directory workDir = _tmpDir.createTempSync('tmp-');
File inputFile = File('${workDir.path}/base.apk'); File inputFile = File('${workDir.path}/base.apk');
@@ -160,25 +179,29 @@ class PatcherAPI {
_outFile = File('${workDir.path}/out.apk'); _outFile = File('${workDir.path}/out.apk');
Directory cacheDir = Directory('${workDir.path}/cache'); Directory cacheDir = Directory('${workDir.path}/cache');
cacheDir.createSync(); cacheDir.createSync();
await patcherChannel.invokeMethod( try {
'runPatcher', await patcherChannel.invokeMethod(
{ 'runPatcher',
'patchBundleFilePath': patchBundleFile.path, {
'originalFilePath': await getOriginalFilePath( 'patchBundleFilePath': patchBundleFile.path,
packageName, 'originalFilePath': await getOriginalFilePath(
originalFilePath, packageName,
), originalFilePath,
'inputFilePath': inputFile.path, ),
'patchedFilePath': patchedFile.path, 'inputFilePath': inputFile.path,
'outFilePath': _outFile!.path, 'patchedFilePath': patchedFile.path,
'integrationsPath': mergeIntegrations ? integrationsFile!.path : '', 'outFilePath': _outFile!.path,
'selectedPatches': selectedPatches.map((p) => p.name).toList(), 'integrationsPath': mergeIntegrations ? integrationsFile!.path : '',
'cacheDirPath': cacheDir.path, 'selectedPatches': selectedPatches.map((p) => p.name).toList(),
'mergeIntegrations': mergeIntegrations, 'cacheDirPath': cacheDir.path,
'resourcePatching': resourcePatching, 'mergeIntegrations': mergeIntegrations,
'keyStoreFilePath': _keyStoreFile.path, 'keyStoreFilePath': _keyStoreFile.path,
}, },
); );
} on Exception catch (e, s) {
print(e);
throw await Sentry.captureException(e, stackTrace: s);
}
} }
} }
@@ -198,22 +221,56 @@ class PatcherAPI {
await AppInstaller.installApk(_outFile!.path); await AppInstaller.installApk(_outFile!.path);
return await DeviceApps.isAppInstalled(patchedApp.packageName); return await DeviceApps.isAppInstalled(patchedApp.packageName);
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false; return false;
} }
} }
return false; return false;
} }
void exportPatchedFile(String appName, String version) {
try {
if (_outFile != null) {
String newName = _getFileName(appName, version);
// This is temporary workaround to populate initial file name
// ref: https://github.com/Cleveroad/cr_file_saver/issues/7
int lastSeparator = _outFile!.path.lastIndexOf('/');
String newSourcePath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
_outFile!.copySync(newSourcePath);
CRFileSaver.saveFileWithDialog(SaveFileDialogParams(
sourceFilePath: newSourcePath,
destinationFileName: newName
));
}
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
}
}
void sharePatchedFile(String appName, String version) { void sharePatchedFile(String appName, String version) {
if (_outFile != null) { try {
if (_outFile != null) {
String newName = _getFileName(appName, version);
int lastSeparator = _outFile!.path.lastIndexOf('/');
String newPath =
_outFile!.path.substring(0, lastSeparator + 1) + newName;
File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
}
}
String _getFileName(String appName, String version) {
String prefix = appName.toLowerCase().replaceAll(' ', '-'); String prefix = appName.toLowerCase().replaceAll(' ', '-');
String newName = '$prefix-revanced_v$version.apk'; String newName = '$prefix-revanced_v$version.apk';
int lastSeparator = _outFile!.path.lastIndexOf('/'); return newName;
String newPath = _outFile!.path.substring(0, lastSeparator + 1) + newName;
File shareFile = _outFile!.copySync(newPath);
ShareExtend.share(shareFile.path, 'file');
}
} }
Future<void> sharePatcherLog(String logs) async { Future<void> sharePatcherLog(String logs) async {

View File

@@ -1,37 +1,56 @@
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:native_dio_client/native_dio_client.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:dio_http_cache_lts/dio_http_cache_lts.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/utils/check_for_gms.dart';
import 'package:timeago/timeago.dart'; import 'package:timeago/timeago.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_dio/sentry_dio.dart';
@lazySingleton @lazySingleton
class RevancedAPI { class RevancedAPI {
late Dio _dio = Dio(); late Dio _dio = Dio();
final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig());
final Options _cacheOptions = buildCacheOptions( final Options _cacheOptions = buildCacheOptions(
const Duration(days: 1), const Duration(hours: 6),
maxStale: const Duration(days: 7), maxStale: const Duration(days: 1),
); );
Future<void> initialize(String apiUrl) async { Future<void> initialize(String apiUrl) async {
_dio = Dio(BaseOptions( try {
baseUrl: apiUrl, bool isGMSInstalled = await checkForGMS();
))
..httpClientAdapter = Http2Adapter( if (!isGMSInstalled) {
ConnectionManager( _dio = Dio(BaseOptions(
idleTimeout: 10000, baseUrl: apiUrl,
onClientCreate: (_, config) => config.onBadCertificate = (_) => true, ));
), print('ReVanced API: Using default engine + $isGMSInstalled');
} else {
_dio = Dio(BaseOptions(
baseUrl: apiUrl,
))
..httpClientAdapter = NativeAdapter();
print('ReVanced API: Using CronetEngine + $isGMSInstalled');
}
_dio.interceptors.add(_dioCacheManager.interceptor);
_dio.addSentry(
captureFailedRequests: true,
); );
_dio.interceptors.add(_dioCacheManager.interceptor); } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
Future<void> clearAllCache() async { Future<void> clearAllCache() async {
await _dioCacheManager.clearAll(); try {
await _dioCacheManager.clearAll();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
Future<Map<String, List<dynamic>>> getContributors() async { Future<Map<String, List<dynamic>>> getContributors() async {
@@ -43,7 +62,8 @@ class RevancedAPI {
String name = repo['name']; String name = repo['name'];
contributors[name] = repo['contributors']; contributors[name] = repo['contributors'];
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return {}; return {};
} }
return contributors; return contributors;
@@ -54,7 +74,8 @@ class RevancedAPI {
var response = await _dio.get('/patches', options: _cacheOptions); var response = await _dio.get('/patches', options: _cacheOptions);
List<dynamic> patches = response.data; List<dynamic> patches = response.data;
return patches.map((patch) => Patch.fromJson(patch)).toList(); return patches.map((patch) => Patch.fromJson(patch)).toList();
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return List.empty(); return List.empty();
} }
} }
@@ -71,7 +92,8 @@ class RevancedAPI {
t['repository'] == repoName && t['repository'] == repoName &&
(t['name'] as String).endsWith(extension), (t['name'] as String).endsWith(extension),
); );
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
} }
@@ -88,7 +110,8 @@ class RevancedAPI {
if (release != null) { if (release != null) {
return release['version']; return release['version'];
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
return null; return null;
@@ -104,7 +127,8 @@ class RevancedAPI {
String url = release['browser_download_url']; String url = release['browser_download_url'];
return await DefaultCacheManager().getSingleFile(url); return await DefaultCacheManager().getSingleFile(url);
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
return null; return null;
@@ -123,7 +147,8 @@ class RevancedAPI {
DateTime timestamp = DateTime.parse(release['timestamp'] as String); DateTime timestamp = DateTime.parse(release['timestamp'] as String);
return format(timestamp, locale: 'en_short'); return format(timestamp, locale: 'en_short');
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null; return null;
} }
return null; return null;

View File

@@ -1,10 +1,21 @@
import 'package:root/root.dart'; import 'package:root/root.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class RootAPI { class RootAPI {
final String _managerDirPath = '/data/local/tmp/revanced-manager'; final String _managerDirPath = '/data/local/tmp/revanced-manager';
final String _postFsDataDirPath = '/data/adb/post-fs-data.d'; final String _postFsDataDirPath = '/data/adb/post-fs-data.d';
final String _serviceDDirPath = '/data/adb/service.d'; final String _serviceDDirPath = '/data/adb/service.d';
Future<bool> isRooted() async {
try {
bool? isRooted = await Root.isRootAvailable();
return isRooted != null && isRooted;
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false;
}
}
Future<bool> hasRootPermissions() async { Future<bool> hasRootPermissions() async {
try { try {
bool? isRooted = await Root.isRootAvailable(); bool? isRooted = await Root.isRootAvailable();
@@ -13,7 +24,8 @@ class RootAPI {
return isRooted != null && isRooted; return isRooted != null && isRooted;
} }
return false; return false;
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false; return false;
} }
} }
@@ -66,7 +78,8 @@ class RootAPI {
apps.removeWhere((pack) => pack.isEmpty); apps.removeWhere((pack) => pack.isEmpty);
return apps.map((pack) => pack.trim()).toList(); return apps.map((pack) => pack.trim()).toList();
} }
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return List.empty(); return List.empty();
} }
return List.empty(); return List.empty();
@@ -112,14 +125,15 @@ class RootAPI {
await installApk(packageName, patchedFilePath); await installApk(packageName, patchedFilePath);
await mountApk(packageName, originalFilePath); await mountApk(packageName, originalFilePath);
return true; return true;
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false; return false;
} }
} }
Future<void> installServiceDScript(String packageName) async { Future<void> installServiceDScript(String packageName) async {
String content = '#!/system/bin/sh\n' String content = '#!/system/bin/sh\n'
'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 1; done\n' 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n'
'base_path=$_managerDirPath/$packageName/base.apk\n' 'base_path=$_managerDirPath/$packageName/base.apk\n'
'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n'
'[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path'; '[ ! -z \$stock_path ] && mount -o bind \$base_path \$stock_path';

View File

@@ -20,4 +20,15 @@ class Toast {
gravity: t.ToastGravity.CENTER, gravity: t.ToastGravity.CENTER,
); );
} }
void showBottom(String text) {
t.Fluttertoast.showToast(
msg: FlutterI18n.translate(
_fToast.context!,
text,
),
toastLength: t.Toast.LENGTH_LONG,
gravity: t.ToastGravity.BOTTOM,
);
}
} }

View File

@@ -2,11 +2,13 @@ import 'dart:io';
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
class AppSelectorViewModel extends BaseViewModel { class AppSelectorViewModel extends BaseViewModel {
@@ -26,12 +28,13 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication( locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: application.apkFilePath, apkFilePath: application.apkFilePath,
icon: application.icon, icon: application.icon,
patchDate: DateTime.now(), patchDate: DateTime.now(),
); );
locator<PatcherViewModel>().selectedPatches.clear(); locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }
@@ -43,6 +46,15 @@ class AppSelectorViewModel extends BaseViewModel {
); );
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
File apkFile = File(result.files.single.path!); File apkFile = File(result.files.single.path!);
List<String> pathSplit = result.files.single.path!.split("/");
pathSplit.removeLast();
Directory filePickerCacheDir = Directory(pathSplit.join("/"));
Iterable<File> deletableFiles =
(await filePickerCacheDir.list().toList()).whereType<File>();
for (var file in deletableFiles) {
if (file.path != apkFile.path && file.path.endsWith(".apk"))
file.delete();
}
ApplicationWithIcon? application = await DeviceApps.getAppFromStorage( ApplicationWithIcon? application = await DeviceApps.getAppFromStorage(
apkFile.path, apkFile.path,
true, true,
@@ -51,18 +63,20 @@ class AppSelectorViewModel extends BaseViewModel {
locator<PatcherViewModel>().selectedApp = PatchedApplication( locator<PatcherViewModel>().selectedApp = PatchedApplication(
name: application.appName, name: application.appName,
packageName: application.packageName, packageName: application.packageName,
originalPackageName: application.packageName,
version: application.versionName!, version: application.versionName!,
apkFilePath: result.files.single.path!, apkFilePath: result.files.single.path!,
icon: application.icon, icon: application.icon,
patchDate: DateTime.now(), patchDate: DateTime.now(),
isFromStorage: true, isFromStorage: true,
); );
locator<PatcherViewModel>().selectedPatches.clear(); locator<PatcherViewModel>().loadLastSelectedPatches();
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }
} }
} on Exception { } on Exception catch (e, s) {
_toast.show('appSelectorView.errorMessage'); await Sentry.captureException(e, stackTrace: s);
_toast.showBottom('appSelectorView.errorMessage');
} }
} }

View File

@@ -67,20 +67,20 @@ class HomeView extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: <Widget>[ children: <Widget>[
DashboardChip( CustomChip(
label: I18nText('homeView.updatesAvailable'),
isSelected: model.showUpdatableApps,
onSelected: (value) {
model.toggleUpdatableApps(true);
},
),
const SizedBox(width: 10),
DashboardChip(
label: I18nText('homeView.installed'), label: I18nText('homeView.installed'),
isSelected: !model.showUpdatableApps, isSelected: !model.showUpdatableApps,
onSelected: (value) { onSelected: (value) {
model.toggleUpdatableApps(false); model.toggleUpdatableApps(false);
}, },
),
const SizedBox(width: 10),
CustomChip(
label: I18nText('homeView.updatesAvailable'),
isSelected: model.showUpdatableApps,
onSelected: (value) {
model.toggleUpdatableApps(true);
},
) )
], ],
), ),

View File

@@ -16,6 +16,7 @@ import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
@@ -28,7 +29,7 @@ class HomeViewModel extends BaseViewModel {
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
DateTime? _lastUpdate; DateTime? _lastUpdate;
bool showUpdatableApps = true; bool showUpdatableApps = false;
List<PatchedApplication> patchedInstalledApps = []; List<PatchedApplication> patchedInstalledApps = [];
List<PatchedApplication> patchedUpdatableApps = []; List<PatchedApplication> patchedUpdatableApps = [];
@@ -46,7 +47,7 @@ class HomeViewModel extends BaseViewModel {
?.requestPermission(); ?.requestPermission();
bool isConnected = await Connectivity().checkConnection(); bool isConnected = await Connectivity().checkConnection();
if (!isConnected) { if (!isConnected) {
_toast.show('homeView.noConnection'); _toast.showBottom('homeView.noConnection');
} }
_getPatchedApps(); _getPatchedApps();
_managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps());
@@ -94,7 +95,8 @@ class HomeViewModel extends BaseViewModel {
int currentVersionInt = int currentVersionInt =
int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), '')); int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), ''));
return latestVersionInt > currentVersionInt; return latestVersionInt > currentVersionInt;
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return false; return false;
} }
} }
@@ -103,7 +105,7 @@ class HomeViewModel extends BaseViewModel {
Future<void> updateManager(BuildContext context) async { Future<void> updateManager(BuildContext context) async {
try { try {
_toast.show('homeView.downloadingMessage'); _toast.showBottom('homeView.downloadingMessage');
File? managerApk = await _managerAPI.downloadManager(); File? managerApk = await _managerAPI.downloadManager();
if (managerApk != null) { if (managerApk != null) {
await flutterLocalNotificationsPlugin.zonedSchedule( await flutterLocalNotificationsPlugin.zonedSchedule(
@@ -130,16 +132,21 @@ class HomeViewModel extends BaseViewModel {
uiLocalNotificationDateInterpretation: uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime, UILocalNotificationDateInterpretation.absoluteTime,
); );
_toast.show('homeView.installingMessage'); _toast.showBottom('homeView.installingMessage');
await AppInstaller.installApk(managerApk.path); await AppInstaller.installApk(managerApk.path);
} else { } else {
_toast.show('homeView.errorDownloadMessage'); _toast.showBottom('homeView.errorDownloadMessage');
} }
} on Exception { } on Exception catch (e, s) {
_toast.show('homeView.errorInstallMessage'); await Sentry.captureException(e, stackTrace: s);
_toast.showBottom('homeView.errorInstallMessage');
} }
} }
void updatesAreDisabled() {
_toast.showBottom('homeView.updatesDisabled');
}
Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async { Future<void> showUpdateConfirmationDialog(BuildContext parentContext) async {
return showDialog( return showDialog(
context: parentContext, context: parentContext,
@@ -176,7 +183,7 @@ class HomeViewModel extends BaseViewModel {
Future<void> forceRefresh(BuildContext context) async { Future<void> forceRefresh(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
if (_lastUpdate == null || if (_lastUpdate == null ||
_lastUpdate!.difference(DateTime.now()).inSeconds > 60) { _lastUpdate!.difference(DateTime.now()).inSeconds > 2) {
_managerAPI.clearAllData(); _managerAPI.clearAllData();
} }
initialize(context); initialize(context);

View File

@@ -18,130 +18,143 @@ class InstallerView extends StatelessWidget {
onModelReady: (model) => model.initialize(context), onModelReady: (model) => model.initialize(context),
viewModelBuilder: () => InstallerViewModel(), viewModelBuilder: () => InstallerViewModel(),
builder: (context, model, child) => WillPopScope( builder: (context, model, child) => WillPopScope(
child: Scaffold( child: SafeArea(
body: CustomScrollView( top: false,
controller: model.scrollController, child: Scaffold(
slivers: <Widget>[ body: CustomScrollView(
CustomSliverAppBar( controller: model.scrollController,
title: Text( slivers: <Widget>[
model.headerLogs, CustomSliverAppBar(
style: GoogleFonts.inter( title: Text(
color: Theme.of(context).textTheme.headline6!.color, model.headerLogs,
), style: GoogleFonts.inter(
), color: Theme.of(context).textTheme.headline6!.color,
onBackButtonPressed: () => model.onWillPop(context),
actions: <Widget>[
Visibility(
visible: !model.isPatching && !model.hasErrors,
child: CustomPopupMenu(
onSelected: (value) => model.onMenuSelection(value),
children: {
0: I18nText(
'installerView.shareApkMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
1: I18nText(
'installerView.shareLogMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
},
), ),
), ),
], onBackButtonPressed: () => model.onWillPop(context),
bottom: PreferredSize( actions: <Widget>[
preferredSize: const Size(double.infinity, 1.0), Visibility(
child: visible: !model.isPatching,
GradientProgressIndicator(progress: model.progress!)), child: CustomPopupMenu(
), onSelected: (value) => model.onMenuSelection(value),
SliverPadding( children: {
padding: const EdgeInsets.all(20.0), if (!model.hasErrors)
sliver: SliverList( 0: I18nText(
delegate: SliverChildListDelegate.fixed( 'installerView.shareApkMenuOption',
<Widget>[ child: const Text(
CustomCard( '',
child: Text( style: TextStyle(
model.logs, fontWeight: FontWeight.bold,
style: GoogleFonts.jetBrainsMono( ),
fontSize: 13, ),
height: 1.5, ),
1: I18nText(
'installerView.exportApkMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
2: I18nText(
'installerView.shareLogMenuOption',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
},
),
),
],
bottom: PreferredSize(
preferredSize: const Size(double.infinity, 1.0),
child:
GradientProgressIndicator(progress: model.progress!)),
),
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
<Widget>[
CustomCard(
child: Text(
model.logs,
style: GoogleFonts.jetBrainsMono(
fontSize: 13,
height: 1.5,
),
), ),
), ),
), ],
], ),
), ),
), ),
), SliverFillRemaining(
SliverFillRemaining( hasScrollBody: false,
hasScrollBody: false, child: Align(
child: Align( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: Visibility(
child: Visibility( visible: !model.isPatching && !model.hasErrors,
visible: !model.isPatching && !model.hasErrors, child: Padding(
child: Padding( padding: const EdgeInsets.all(20.0).copyWith(top: 0.0),
padding: const EdgeInsets.all(20.0).copyWith(top: 0.0), child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
children: <Widget>[ Visibility(
Visibility( visible: model.isInstalled,
visible: model.isInstalled, child: CustomMaterialButton(
child: CustomMaterialButton( label: I18nText('installerView.openButton'),
label: I18nText('installerView.openButton'), isExpanded: true,
isExpanded: true, onPressed: () {
onPressed: () { model.openApp();
model.openApp(); model.cleanPatcher();
model.cleanPatcher(); Navigator.of(context).pop();
Navigator.of(context).pop(); },
},
),
),
Visibility(
visible: !model.isInstalled,
child: CustomMaterialButton(
isFilled: false,
label:
I18nText('installerView.installRootButton'),
isExpanded: true,
onPressed: () => model.installResult(
context,
true,
), ),
), ),
), Visibility(
Visibility( visible: !model.isInstalled && model.isRooted,
visible: !model.isInstalled, child: CustomMaterialButton(
child: const SizedBox( isFilled: false,
width: 16, label:
), I18nText('installerView.installRootButton'),
), isExpanded: true,
Visibility( onPressed: () => model.installResult(
visible: !model.isInstalled, context,
child: CustomMaterialButton( true,
label: I18nText('installerView.installButton'), ),
isExpanded: true,
onPressed: () => model.installResult(
context,
false,
), ),
), ),
), Visibility(
], visible: !model.isInstalled,
child: const SizedBox(
width: 16,
),
),
Visibility(
visible: !model.isInstalled,
child: CustomMaterialButton(
label: I18nText('installerView.installButton'),
isExpanded: true,
onPressed: () => model.installResult(
context,
false,
),
),
),
],
),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
onWillPop: () => model.onWillPop(context), onWillPop: () => model.onWillPop(context),

View File

@@ -1,3 +1,4 @@
// ignore_for_file: use_build_context_synchronously
import 'package:device_apps/device_apps.dart'; import 'package:device_apps/device_apps.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -9,15 +10,18 @@ import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
class InstallerViewModel extends BaseViewModel { class InstallerViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI();
final Toast _toast = locator<Toast>(); final Toast _toast = locator<Toast>();
final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!; final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!;
final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches; final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches;
@@ -28,11 +32,13 @@ class InstallerViewModel extends BaseViewModel {
double? progress = 0.0; double? progress = 0.0;
String logs = ''; String logs = '';
String headerLogs = ''; String headerLogs = '';
bool isRooted = false;
bool isPatching = true; bool isPatching = true;
bool isInstalled = false; bool isInstalled = false;
bool hasErrors = false; bool hasErrors = false;
Future<void> initialize(BuildContext context) async { Future<void> initialize(BuildContext context) async {
isRooted = await _rootAPI.isRooted();
if (await Permission.ignoreBatteryOptimizations.isGranted) { if (await Permission.ignoreBatteryOptimizations.isGranted) {
try { try {
FlutterBackground.initialize( FlutterBackground.initialize(
@@ -52,7 +58,8 @@ class InstallerViewModel extends BaseViewModel {
), ),
), ),
).then((value) => FlutterBackground.enableBackgroundExecution()); ).then((value) => FlutterBackground.enableBackgroundExecution());
} on Exception { } on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
// ignore // ignore
} }
} }
@@ -116,91 +123,114 @@ class InstallerViewModel extends BaseViewModel {
} }
Future<void> runPatcher() async { Future<void> runPatcher() async {
update(0.0, 'Initializing...', 'Initializing installer'); try {
if (_patches.isNotEmpty) { update(0.0, 'Initializing...', 'Initializing installer');
try { if (_patches.isNotEmpty) {
update(0.1, '', 'Creating working directory'); try {
await _patcherAPI.runPatcher( update(0.1, '', 'Creating working directory');
_app.packageName, await _patcherAPI.runPatcher(
_app.apkFilePath, _app.packageName,
_patches, _app.apkFilePath,
); _patches,
} catch (e) { );
update( } on Exception catch (e, s) {
-100.0, update(
'Aborting...', -100.0,
'An error occurred! Aborting\nError:\n$e', 'Aborting...',
); 'An error occurred! Aborting\nError:\n$e',
);
await Sentry.captureException(e, stackTrace: s);
throw await Sentry.captureException(e, stackTrace: s);
}
} else {
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting');
} }
} else { if (FlutterBackground.isBackgroundExecutionEnabled) {
update(-100.0, 'Aborting...', 'No app or patches selected! Aborting'); try {
} FlutterBackground.disableBackgroundExecution();
if (FlutterBackground.isBackgroundExecutionEnabled) { } on Exception catch (e, s) {
try { await Sentry.captureException(e, stackTrace: s);
FlutterBackground.disableBackgroundExecution(); // ignore
} on Exception { }
// ignore
} }
await Wakelock.disable();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
} }
await Wakelock.disable();
} }
void installResult(BuildContext context, bool installAsRoot) async { void installResult(BuildContext context, bool installAsRoot) async {
_app.isRooted = installAsRoot; try {
bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support')); _app.isRooted = installAsRoot;
bool rootMicroG = installAsRoot && hasMicroG; bool hasMicroG = _patches.any((p) => p.name.endsWith('microg-support'));
bool rootFromStorage = installAsRoot && _app.isFromStorage; bool rootMicroG = installAsRoot && hasMicroG;
bool ytWithoutRootMicroG = bool rootFromStorage = installAsRoot && _app.isFromStorage;
!installAsRoot && !hasMicroG && _app.packageName.contains('youtube'); bool ytWithoutRootMicroG =
if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) { !installAsRoot && !hasMicroG && _app.packageName.contains('youtube');
return showDialog( if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) {
context: context, return showDialog(
builder: (context) => AlertDialog( context: context,
title: I18nText('installerView.installErrorDialogTitle'), builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, title: I18nText('installerView.installErrorDialogTitle'),
content: I18nText( backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
rootMicroG content: I18nText(
? 'installerView.installErrorDialogText1' rootMicroG
: rootFromStorage ? 'installerView.installErrorDialogText1'
? 'installerView.installErrorDialogText3' : rootFromStorage
: 'installerView.installErrorDialogText2', ? 'installerView.installErrorDialogText3'
: 'installerView.installErrorDialogText2',
),
actions: <Widget>[
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () => Navigator.of(context).pop(),
)
],
), ),
actions: <Widget>[ );
CustomMaterialButton( } else {
label: I18nText('okButton'), update(
onPressed: () => Navigator.of(context).pop(), 1.0,
) 'Installing...',
], _app.isRooted
), ? 'Installing patched file using root method'
); : 'Installing patched file using nonroot method',
} else { );
update( isInstalled = await _patcherAPI.installPatchedFile(_app);
1.0, if (isInstalled) {
'Installing...', update(1.0, 'Installed!', 'Installed!');
_app.isRooted _app.isFromStorage = false;
? 'Installing patched file using root method' _app.patchDate = DateTime.now();
: 'Installing patched file using nonroot method', _app.appliedPatches = _patches.map((p) => p.name).toList();
); if (hasMicroG) {
isInstalled = await _patcherAPI.installPatchedFile(_app); _app.name += ' ReVanced';
if (isInstalled) { _app.packageName = _app.packageName.replaceFirst(
update(1.0, 'Installed!', 'Installed!'); 'com.google.',
_app.isFromStorage = false; 'app.revanced.',
_app.patchDate = DateTime.now(); );
_app.appliedPatches = _patches.map((p) => p.name).toList(); }
if (hasMicroG) { await _managerAPI.savePatchedApp(_app);
_app.name += ' ReVanced';
_app.packageName = _app.packageName.replaceFirst(
'com.google.',
'app.revanced.',
);
} }
await _managerAPI.savePatchedApp(_app);
} }
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
}
void exportResult() {
try {
_patcherAPI.exportPatchedFile(_app.name, _app.version);
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
} }
} }
void shareResult() { void shareResult() {
_patcherAPI.sharePatchedFile(_app.name, _app.version); try {
_patcherAPI.sharePatchedFile(_app.name, _app.version);
} on Exception catch (e, s) {
Sentry.captureException(e, stackTrace: s);
}
} }
void shareLog() { void shareLog() {
@@ -208,10 +238,14 @@ class InstallerViewModel extends BaseViewModel {
} }
Future<void> cleanPatcher() async { Future<void> cleanPatcher() async {
_patcherAPI.cleanPatcher(); try {
locator<PatcherViewModel>().selectedApp = null; _patcherAPI.cleanPatcher();
locator<PatcherViewModel>().selectedPatches.clear(); locator<PatcherViewModel>().selectedApp = null;
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().selectedPatches.clear();
locator<PatcherViewModel>().notifyListeners();
} on Exception catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
}
} }
void openApp() { void openApp() {
@@ -224,6 +258,9 @@ class InstallerViewModel extends BaseViewModel {
shareResult(); shareResult();
break; break;
case 1: case 1:
exportResult();
break;
case 2:
shareLog(); shareLog();
break; break;
} }
@@ -231,7 +268,7 @@ class InstallerViewModel extends BaseViewModel {
Future<bool> onWillPop(BuildContext context) async { Future<bool> onWillPop(BuildContext context) async {
if (isPatching) { if (isPatching) {
_toast.show('installerView.noExit'); _toast.showBottom('installerView.noExit');
return false; return false;
} }
cleanPatcher(); cleanPatcher();

View File

@@ -58,7 +58,7 @@ class PatcherViewModel extends BaseViewModel {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('patcherView.patchDialogTitle'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patcherView.patchDialogText'), content: I18nText('patcherView.patchDialogText'),
actions: <Widget>[ actions: <Widget>[
@@ -107,4 +107,16 @@ class PatcherViewModel extends BaseViewModel {
'appSelectorCard.recommendedVersion', 'appSelectorCard.recommendedVersion',
)}: $recommendedVersion'; )}: $recommendedVersion';
} }
Future<void> loadLastSelectedPatches() async {
this.selectedPatches.clear();
List<String> selectedPatches =
await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName);
List<Patch> patches =
await _patcherAPI.getFilteredPatches(selectedApp!.originalPackageName);
this
.selectedPatches
.addAll(patches.where((patch) => selectedPatches.contains(patch.name)));
notifyListeners();
}
} }

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart'; import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_chip.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart';
import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/search_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -25,7 +27,12 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
floatingActionButton: Visibility( floatingActionButton: Visibility(
visible: model.patches.isNotEmpty, visible: model.patches.isNotEmpty,
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
label: I18nText('patchesSelectorView.doneButton'), label: Row(
children: <Widget>[
I18nText('patchesSelectorView.doneButton'),
Text(' (${model.selectedPatches.length})')
],
),
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
onPressed: () { onPressed: () {
model.selectPatches(); model.selectPatches();
@@ -55,6 +62,39 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
actions: [
Container(
height: 2,
margin: const EdgeInsets.only(top: 12, bottom: 12),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
model.patchesVersion!,
style: TextStyle(
color: Theme.of(context).textTheme.headline6!.color,
),
),
),
CustomPopupMenu(
onSelected: (value) => {model.onMenuSelection(value)},
children: {
0: I18nText(
'patchesSelectorView.loadPatchesSelection',
child: const Text(
'',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
},
),
],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(64.0), preferredSize: const Size.fromHeight(64.0),
child: Padding( child: Padding(
@@ -101,103 +141,51 @@ class _PatchesSelectorViewState extends State<PatchesSelectorView> {
padding: const EdgeInsets.symmetric(horizontal: 12.0) padding: const EdgeInsets.symmetric(horizontal: 12.0)
.copyWith(bottom: 80), .copyWith(bottom: 80),
child: Column( child: Column(
children: model children: [
.getQueriedPatches(_query) Row(
.map( children: [
(patch) => PatchItem( CustomChip(
name: patch.name, label:
simpleName: patch.getSimpleName(), I18nText('patchesSelectorView.recommended'),
version: patch.version, onSelected: (value) {
description: patch.description, model.selectRecommendedPatches();
packageVersion: model.getAppVersion(), },
supportedPackageVersions:
model.getSupportedVersions(patch),
isUnsupported: !model.isPatchSupported(patch),
isSelected: model.isSelected(patch),
onChanged: (value) =>
model.selectPatch(patch, value),
), ),
/* TODO: Enable this and make use of new Patch Options implementation const SizedBox(width: 8),
patch.hasOptions ? ExpandablePanel( CustomChip(
controller: expController, label: I18nText('patchesSelectorView.all'),
theme: const ExpandableThemeData( onSelected: (value) {
hasIcon: false, model.selectAllPatches(true);
tapBodyToExpand: true, },
tapBodyToCollapse: true, ),
tapHeaderToExpand: true, const SizedBox(width: 8),
), CustomChip(
header: Column( label: I18nText('patchesSelectorView.none'),
children: <Widget>[ onSelected: (value) {
GestureDetector( model.clearPatches();
onLongPress: () => },
expController.toggle(), ),
child: PatchItem( ],
name: patch.name, ),
simpleName: patch.getSimpleName(), ...model
description: patch.description, .getQueriedPatches(_query)
version: patch.version, .map(
packageVersion: (patch) => PatchItem(
model.getAppVersion(), name: patch.name,
supportedPackageVersions: model simpleName: patch.getSimpleName(),
.getSupportedVersions(patch), version: patch.version,
isUnsupported: !model description: patch.description,
.isPatchSupported(patch), packageVersion: model.getAppVersion(),
isSelected: supportedPackageVersions:
model.isSelected(patch), model.getSupportedVersions(patch),
onChanged: (value) => model isUnsupported: !model.isPatchSupported(patch),
.selectPatch(patch, value), isSelected: model.isSelected(patch),
child: const Padding( onChanged: (value) =>
padding: EdgeInsets.symmetric( model.selectPatch(patch, value),
vertical: 8.0, ),
), )
child: Text( .toList(),
'Long press for additional options.', ],
),
),
),
),
],
),
expanded: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 10,
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.tertiary
.withOpacity(0.1),
borderRadius:
BorderRadius.circular(12),
),
child: Column(
children: <Widget>[
Text(
'Patch options',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const OptionsTextField(
hint: 'App name'),
const OptionsFilePicker(
optionName: 'Choose a logo',
),
],
),
),
),
collapsed: Container(),
) */
)
.toList(),
), ),
), ),
), ),

View File

@@ -3,7 +3,10 @@ import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/models/patch.dart';
import 'package:revanced_manager/models/patched_application.dart'; import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/github_api.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -11,15 +14,23 @@ import 'package:flutter/material.dart';
class PatchesSelectorViewModel extends BaseViewModel { class PatchesSelectorViewModel extends BaseViewModel {
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final GithubAPI _githubAPI = locator<GithubAPI>();
final List<Patch> patches = []; final List<Patch> patches = [];
final List<Patch> selectedPatches = final List<Patch> selectedPatches =
locator<PatcherViewModel>().selectedPatches; locator<PatcherViewModel>().selectedPatches;
String? patchesVersion = '';
bool isDefaultPatchesRepo() {
return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches';
}
Future<void> initialize() async { Future<void> initialize() async {
getPatchesVersion();
patches.addAll(await _patcherAPI.getFilteredPatches( patches.addAll(await _patcherAPI.getFilteredPatches(
locator<PatcherViewModel>().selectedApp!.packageName, locator<PatcherViewModel>().selectedApp!.originalPackageName,
)); ));
patches.sort((a, b) => a.name.compareTo(b.name)); patches.sort((a, b) => a.name.compareTo(b.name));
selectRecommendedPatches();
notifyListeners(); notifyListeners();
} }
@@ -42,7 +53,7 @@ class PatchesSelectorViewModel extends BaseViewModel {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('patchesSelectorView.selectAllPatchesWarningTitle'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'), content: I18nText('patchesSelectorView.selectAllPatchesWarningContent'),
actions: <Widget>[ actions: <Widget>[
@@ -57,25 +68,65 @@ class PatchesSelectorViewModel extends BaseViewModel {
void selectAllPatches(bool isSelected) { void selectAllPatches(bool isSelected) {
selectedPatches.clear(); selectedPatches.clear();
if (isSelected) {
if (isSelected && _managerAPI.areExperimentalPatchesEnabled() == false) {
selectedPatches
.addAll(patches.where((element) => isPatchSupported(element)));
}
if (isSelected && _managerAPI.areExperimentalPatchesEnabled()) {
selectedPatches.addAll(patches); selectedPatches.addAll(patches);
} }
notifyListeners();
}
void selectRecommendedPatches() {
selectedPatches.clear();
if (_managerAPI.areExperimentalPatchesEnabled() == false) {
selectedPatches.addAll(patches.where(
(element) => element.excluded == false && isPatchSupported(element)));
}
if (_managerAPI.areExperimentalPatchesEnabled()) {
selectedPatches
.addAll(patches.where((element) => element.excluded == false));
}
notifyListeners();
}
void clearPatches() {
selectedPatches.clear();
notifyListeners(); notifyListeners();
} }
void selectPatches() { void selectPatches() {
locator<PatcherViewModel>().selectedPatches = selectedPatches; locator<PatcherViewModel>().selectedPatches = selectedPatches;
saveSelectedPatches();
locator<PatcherViewModel>().notifyListeners(); locator<PatcherViewModel>().notifyListeners();
} }
Future<String?> getPatchesVersion() async {
if (isDefaultPatchesRepo()) {
patchesVersion = await _managerAPI.getLatestPatchesVersion();
// print('Patches version: $patchesVersion');
return patchesVersion ?? '0.0.0';
} else {
// fetch from github
patchesVersion = await _githubAPI
.getLastestReleaseVersion(_managerAPI.getPatchesRepo());
}
}
List<Patch> getQueriedPatches(String query) { List<Patch> getQueriedPatches(String query) {
return patches return patches
.where((patch) => .where((patch) =>
query.isEmpty || query.isEmpty ||
query.length < 2 || query.length < 2 ||
patch.name.toLowerCase().contains( patch.name.toLowerCase().contains(query.toLowerCase()) ||
query.toLowerCase(), patch.getSimpleName().toLowerCase().contains(query.toLowerCase()))
))
.toList(); .toList();
} }
@@ -101,4 +152,33 @@ class PatchesSelectorViewModel extends BaseViewModel {
pack.name == app.packageName && pack.name == app.packageName &&
(pack.versions.isEmpty || pack.versions.contains(app.version))); (pack.versions.isEmpty || pack.versions.contains(app.version)));
} }
void onMenuSelection(value) {
switch (value) {
case 0:
loadSelectedPatches();
break;
}
}
Future<void> saveSelectedPatches() async {
List<String> selectedPatches =
this.selectedPatches.map((patch) => patch.name).toList();
await _managerAPI.setSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName,
selectedPatches);
}
Future<void> loadSelectedPatches() async {
List<String> selectedPatches = await _managerAPI.getSelectedPatches(
locator<PatcherViewModel>().selectedApp!.originalPackageName);
if (selectedPatches.isNotEmpty) {
this.selectedPatches.clear();
this.selectedPatches.addAll(
patches.where((patch) => selectedPatches.contains(patch.name)));
} else {
locator<Toast>().showBottom('patchesSelectorView.noSavedPatches');
}
notifyListeners();
}
} }

View File

@@ -0,0 +1,117 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageApiUrl extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _apiUrlController = TextEditingController();
Future<void> showApiUrlDialog(BuildContext context) async {
String apiUrl = _managerAPI.getApiUrl();
_apiUrlController.text = apiUrl.replaceAll('https://', '');
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.apiURLLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showApiUrlResetDialog(context),
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.api_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _apiUrlController,
label: I18nText('settingsView.selectApiURL'),
hint: apiUrl.split('/')[0],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_apiUrlController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
String apiUrl = _apiUrlController.text;
if (!apiUrl.startsWith('https')) {
apiUrl = 'https://$apiUrl';
}
_managerAPI.setApiUrl(apiUrl);
Navigator.of(context).pop();
},
)
],
),
);
}
Future<void> showApiUrlResetDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.apiURLResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setApiUrl('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
}
}
final sManageApiUrl = SManageApiUrl();
class SManageApiUrlUI extends StatelessWidget {
const SManageApiUrlUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.apiURLLabel',
subtitle: 'settingsView.apiURLHint',
onTap: () => sManageApiUrl.showApiUrlDialog(context),
);
}
}

View File

@@ -0,0 +1,162 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:stacked/stacked.dart';
class SManageSources extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _orgPatSourceController = TextEditingController();
final TextEditingController _patSourceController = TextEditingController();
final TextEditingController _orgIntSourceController = TextEditingController();
final TextEditingController _intSourceController = TextEditingController();
Future<void> showSourcesDialog(BuildContext context) async {
String patchesRepo = _managerAPI.getPatchesRepo();
String integrationsRepo = _managerAPI.getIntegrationsRepo();
_orgPatSourceController.text = patchesRepo.split('/')[0];
_patSourceController.text = patchesRepo.split('/')[1];
_orgIntSourceController.text = integrationsRepo.split('/')[0];
_intSourceController.text = integrationsRepo.split('/')[1];
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.sourcesLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showResetConfirmationDialog(context),
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.extension_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgPatSourceController,
label: I18nText('settingsView.orgPatchesLabel'),
hint: patchesRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
inputController: _patSourceController,
label: I18nText('settingsView.sourcesPatchesLabel'),
hint: patchesRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
CustomTextField(
leadingIcon: Icon(
Icons.merge_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgIntSourceController,
label: I18nText('settingsView.orgIntegrationsLabel'),
hint: integrationsRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.merge_outlined,
color: Colors.transparent,
),
inputController: _intSourceController,
label: I18nText('settingsView.sourcesIntegrationsLabel'),
hint: integrationsRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_orgPatSourceController.clear();
_patSourceController.clear();
_orgIntSourceController.clear();
_intSourceController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
_managerAPI.setPatchesRepo(
'${_orgPatSourceController.text}/${_patSourceController.text}',
);
_managerAPI.setIntegrationsRepo(
'${_orgIntSourceController.text}/${_intSourceController.text}',
);
Navigator.of(context).pop();
},
)
],
),
);
}
Future<void> showResetConfirmationDialog(BuildContext context) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.sourcesResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setPatchesRepo('');
_managerAPI.setIntegrationsRepo('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
}
}
final sManageSources = SManageSources();
class SManageSourcesUI extends StatelessWidget {
const SManageSourcesUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.sourcesLabel',
subtitle: 'settingsView.sourcesLabelHint',
onTap: () => sManageSources.showSourcesDialog(context),
);
}
}

View File

@@ -0,0 +1,98 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/main.dart';
import 'package:revanced_manager/services/crowdin_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacked/stacked.dart';
import 'package:timeago/timeago.dart' as timeago;
final _settingViewModel = SettingsViewModel();
class SUpdateLanguage extends BaseViewModel {
final CrowdinAPI _crowdinAPI = locator<CrowdinAPI>();
final Toast _toast = locator<Toast>();
late SharedPreferences _prefs;
String selectedLanguage = 'English';
String selectedLanguageLocale = prefs.getString('language') ?? 'en_US';
List languages = [];
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
selectedLanguageLocale =
_prefs.getString('language') ?? selectedLanguageLocale;
notifyListeners();
}
Future<void> updateLanguage(BuildContext context, String? value) async {
if (value != null) {
selectedLanguageLocale = value;
_prefs = await SharedPreferences.getInstance();
await _prefs.setString('language', value);
await FlutterI18n.refresh(context, Locale(value));
timeago.setLocaleMessages(value, timeago.EnMessages());
locator<NavigationViewModel>().notifyListeners();
notifyListeners();
}
}
Future<void> initLang() async {
languages = await _crowdinAPI.getLanguages();
languages.sort((a, b) => a['name'].compareTo(b['name']));
notifyListeners();
}
Future<void> showLanguagesDialog(BuildContext parentContext) {
initLang();
return showDialog(
context: parentContext,
builder: (context) => SimpleDialog(
title: I18nText('settingsView.languageLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
children: [
SizedBox(
height: 500,
child: ListView.builder(
itemCount: languages.length,
itemBuilder: (context, index) {
return RadioListTile<String>(
title: Text(languages[index]['name']),
subtitle: Text(languages[index]['locale']),
value: languages[index]['locale'],
groupValue: selectedLanguageLocale,
onChanged: (value) {
selectedLanguage = languages[index]['name'];
_toast.showBottom('settingsView.restartAppForChanges');
updateLanguage(context, value);
Navigator.pop(context);
},
);
},
),
),
],
),
);
}
}
class SUpdateLanguageUI extends StatelessWidget {
const SUpdateLanguageUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.languageLabel',
subtitle: _settingViewModel.sUpdateLanguage.selectedLanguage,
onTap: () =>
_settingViewModel.sUpdateLanguage.showLanguagesDialog(context),
);
}
}

View File

@@ -0,0 +1,116 @@
// ignore_for_file: use_build_context_synchronously
import 'package:dynamic_themes/dynamic_themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:stacked/stacked.dart';
final _settingViewModel = SettingsViewModel();
// ignore: constant_identifier_names
const int ANDROID_12_SDK_VERSION = 31;
class SUpdateTheme extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>();
bool getDynamicThemeStatus() {
return _managerAPI.getUseDynamicTheme();
}
void setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners();
}
bool getDarkThemeStatus() {
return _managerAPI.getUseDarkTheme();
}
void setUseDarkTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDarkTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
);
notifyListeners();
}
}
class SUpdateThemeUI extends StatelessWidget {
const SUpdateThemeUI({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.appearanceSectionTitle',
children: <Widget>[
CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.darkThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.darkThemeHint'),
value: SUpdateTheme().getDarkThemeStatus(),
onTap: (value) => SUpdateTheme().setUseDarkTheme(
context,
value,
),
),
FutureBuilder<int>(
future: _settingViewModel.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible:
snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION,
child: CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(),
onTap: (value) =>
_settingViewModel.sUpdateTheme.setUseDynamicTheme(
context,
value,
),
),
),
),
],
);
}
}

View File

@@ -1,12 +1,15 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_theme.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_info_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
@@ -38,131 +41,18 @@ class SettingsView extends StatelessWidget {
SliverList( SliverList(
delegate: SliverChildListDelegate.fixed( delegate: SliverChildListDelegate.fixed(
<Widget>[ <Widget>[
SettingsSection( SUpdateThemeUI(),
title: 'settingsView.appearanceSectionTitle', SUpdateLanguageUI(),
children: <Widget>[
CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.darkThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.darkThemeHint'),
value: model.getDarkThemeStatus(),
onTap: (value) => model.setUseDarkTheme(
context,
value,
),
),
FutureBuilder<int>(
future: model.getSdkVersion(),
builder: (context, snapshot) => Visibility(
visible: snapshot.hasData &&
snapshot.data! >= ANDROID_12_SDK_VERSION,
child: CustomSwitchTile(
padding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.dynamicThemeLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.dynamicThemeHint'),
value: model.getDynamicThemeStatus(),
onTap: (value) => model.setUseDynamicTheme(
context,
value,
),
),
),
),
],
),
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.languageLabel',
subtitle: 'English',
onTap: () => model.showLanguagesDialog(context),
),
_settingsDivider, _settingsDivider,
SettingsSection( STeamSection(),
title: 'settingsView.teamSectionTitle',
children: <Widget>[
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.contributorsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.contributorsHint'),
onTap: () => model.navigateToContributors(),
),
const SocialMediaWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
_settingsDivider, _settingsDivider,
SettingsSection( SAdvancedSection(),
title: 'settingsView.advancedSectionTitle',
children: <Widget>[
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.apiURLLabel',
subtitle: 'settingsView.apiURLHint',
onTap: () => model.showApiUrlDialog(context),
),
SettingsTileDialog(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: 'settingsView.sourcesLabel',
subtitle: 'settingsView.sourcesLabelHint',
onTap: () => model.showSourcesDialog(context),
),
],
),
_settingsDivider, _settingsDivider,
SettingsSection( SExportSection(),
title: 'settingsView.infoSectionTitle', _settingsDivider,
children: <Widget>[ // SLoggingSection(),
ListTile( // _settingsDivider,
contentPadding: SInfoSection(),
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => model.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
),
], ],
), ),
), ),

View File

@@ -1,316 +1,113 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io'; import 'dart:io';
import 'package:cr_file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:logcat/logcat.dart'; import 'package:logcat/logcat.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/app/app.router.dart'; import 'package:revanced_manager/app/app.router.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_language.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_update_theme.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:share_extend/share_extend.dart'; import 'package:share_extend/share_extend.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:timeago/timeago.dart';
// ignore: constant_identifier_names
const int ANDROID_12_SDK_VERSION = 31;
class SettingsViewModel extends BaseViewModel { class SettingsViewModel extends BaseViewModel {
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService = locator<NavigationService>();
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final TextEditingController _orgPatSourceController = TextEditingController(); final Toast _toast = locator<Toast>();
final TextEditingController _patSourceController = TextEditingController();
final TextEditingController _orgIntSourceController = TextEditingController();
final TextEditingController _intSourceController = TextEditingController();
final TextEditingController _apiUrlController = TextEditingController();
void setLanguage(String language) { final SUpdateLanguage sUpdateLanguage = SUpdateLanguage();
notifyListeners(); final SUpdateTheme sUpdateTheme = SUpdateTheme();
}
void navigateToContributors() { void navigateToContributors() {
_navigationService.navigateTo(Routes.contributorsView); _navigationService.navigateTo(Routes.contributorsView);
} }
Future<void> updateLanguage(BuildContext context, String? value) async { bool isSentryEnabled() {
if (value != null) { return _managerAPI.isSentryEnabled();
await FlutterI18n.refresh(context, Locale(value));
setLocaleMessages(value, EnMessages());
}
} }
bool getDynamicThemeStatus() { void useSentry(bool value) {
return _managerAPI.getUseDynamicTheme(); _managerAPI.setSentryStatus(value);
} _toast.showBottom('settingsView.restartAppForChanges');
void setUseDynamicTheme(BuildContext context, bool value) async {
await _managerAPI.setUseDynamicTheme(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme.isEven) {
await DynamicTheme.of(context)!.setTheme(value ? 2 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 1);
}
notifyListeners(); notifyListeners();
} }
bool getDarkThemeStatus() { bool areExperimentalPatchesEnabled() {
return _managerAPI.getUseDarkTheme(); return _managerAPI.areExperimentalPatchesEnabled();
} }
void setUseDarkTheme(BuildContext context, bool value) async { void useExperimentalPatches(bool value) {
await _managerAPI.setUseDarkTheme(value); _managerAPI.enableExperimentalPatchesStatus(value);
int currentTheme = DynamicTheme.of(context)!.themeId;
if (currentTheme < 2) {
await DynamicTheme.of(context)!.setTheme(value ? 1 : 0);
} else {
await DynamicTheme.of(context)!.setTheme(value ? 3 : 2);
}
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
value ? Brightness.light : Brightness.dark,
),
);
notifyListeners(); notifyListeners();
} }
Future<void> showLanguagesDialog(BuildContext context) { void deleteKeystore() {
return showDialog( _managerAPI.deleteKeystore();
context: context, _toast.showBottom('settingsView.deletedKeystore');
builder: (context) => SimpleDialog( notifyListeners();
title: I18nText('settingsView.languageLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
children: <Widget>[
RadioListTile<String>(
title: I18nText('settingsView.englishOption'),
value: 'en',
groupValue: 'en',
onChanged: (value) {
updateLanguage(context, value);
Navigator.of(context).pop();
},
),
],
),
);
} }
Future<void> showSourcesDialog(BuildContext context) async { void deleteTempDir() {
String patchesRepo = _managerAPI.getPatchesRepo(); _managerAPI.deleteTempFolder();
String integrationsRepo = _managerAPI.getIntegrationsRepo(); _toast.showBottom('settingsView.deletedTempDir');
_orgPatSourceController.text = patchesRepo.split('/')[0]; notifyListeners();
_patSourceController.text = patchesRepo.split('/')[1];
_orgIntSourceController.text = integrationsRepo.split('/')[0];
_intSourceController.text = integrationsRepo.split('/')[1];
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: <Widget>[
I18nText('settingsView.sourcesLabel'),
const Spacer(),
IconButton(
icon: const Icon(Icons.manage_history_outlined),
onPressed: () => showResetConfirmationDialog(context),
color: Theme.of(context).colorScheme.secondary,
)
],
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: SingleChildScrollView(
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.extension_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgPatSourceController,
label: I18nText('settingsView.orgPatchesLabel'),
hint: patchesRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.extension_outlined,
color: Colors.transparent,
),
inputController: _patSourceController,
label: I18nText('settingsView.sourcesPatchesLabel'),
hint: patchesRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 20),
CustomTextField(
leadingIcon: Icon(
Icons.merge_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _orgIntSourceController,
label: I18nText('settingsView.orgIntegrationsLabel'),
hint: integrationsRepo.split('/')[0],
onChanged: (value) => notifyListeners(),
),
const SizedBox(height: 8),
CustomTextField(
leadingIcon: const Icon(
Icons.merge_outlined,
color: Colors.transparent,
),
inputController: _intSourceController,
label: I18nText('settingsView.sourcesIntegrationsLabel'),
hint: integrationsRepo.split('/')[1],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_orgPatSourceController.clear();
_patSourceController.clear();
_orgIntSourceController.clear();
_intSourceController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
_managerAPI.setPatchesRepo(
'${_orgPatSourceController.text}/${_patSourceController.text}',
);
_managerAPI.setIntegrationsRepo(
'${_orgIntSourceController.text}/${_intSourceController.text}',
);
Navigator.of(context).pop();
},
)
],
),
);
} }
Future<void> showApiUrlDialog(BuildContext context) async { Future<void> exportPatches() async {
String apiUrl = _managerAPI.getApiUrl(); try {
_apiUrlController.text = apiUrl.replaceAll('https://', ''); File outFile = File(_managerAPI.storedPatchesFile);
return showDialog( if (outFile.existsSync()) {
context: context, String dateTime =
builder: (context) => AlertDialog( DateTime.now().toString().replaceAll(' ', '_').split('.').first;
title: Row( String tempFilePath =
children: <Widget>[ '${outFile.path.substring(0, outFile.path.lastIndexOf('/') + 1)}selected_patches_$dateTime.json';
I18nText('settingsView.apiURLLabel'), outFile.copySync(tempFilePath);
const Spacer(), await CRFileSaver.saveFileWithDialog(SaveFileDialogParams(
IconButton( sourceFilePath: tempFilePath, destinationFileName: ''));
icon: const Icon(Icons.manage_history_outlined), File(tempFilePath).delete();
onPressed: () => showApiUrlResetDialog(context), locator<Toast>().showBottom('settingsView.exportedPatches');
color: Theme.of(context).colorScheme.secondary, } else {
) locator<Toast>().showBottom('settingsView.noExportFileFound');
], }
), } on Exception catch (e, s) {
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, Sentry.captureException(e, stackTrace: s);
content: SingleChildScrollView( }
child: Column(
children: <Widget>[
CustomTextField(
leadingIcon: Icon(
Icons.api_outlined,
color: Theme.of(context).colorScheme.secondary,
),
inputController: _apiUrlController,
label: I18nText('settingsView.selectApiURL'),
hint: apiUrl.split('/')[0],
onChanged: (value) => notifyListeners(),
),
],
),
),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('cancelButton'),
onPressed: () {
_apiUrlController.clear();
Navigator.of(context).pop();
},
),
CustomMaterialButton(
label: I18nText('okButton'),
onPressed: () {
String apiUrl = _apiUrlController.text;
if (!apiUrl.startsWith('https')) {
apiUrl = 'https://$apiUrl';
}
_managerAPI.setApiUrl(apiUrl);
Navigator.of(context).pop();
},
)
],
),
);
} }
Future<void> showResetConfirmationDialog(BuildContext context) async { Future<void> importPatches() async {
return showDialog( try {
context: context, FilePickerResult? result = await FilePicker.platform.pickFiles(
builder: (context) => AlertDialog( type: FileType.custom,
title: I18nText('settingsView.sourcesResetDialogTitle'), allowedExtensions: ['json'],
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, );
content: I18nText('settingsView.sourcesResetDialogText'), if (result != null && result.files.single.path != null) {
actions: <Widget>[ File inFile = File(result.files.single.path!);
CustomMaterialButton( final File storedPatchesFile = File(_managerAPI.storedPatchesFile);
isFilled: false, if (!storedPatchesFile.existsSync()) {
label: I18nText('noButton'), storedPatchesFile.createSync(recursive: true);
onPressed: () => Navigator.of(context).pop(), }
), inFile.copySync(storedPatchesFile.path);
CustomMaterialButton( inFile.delete();
label: I18nText('yesButton'), if (locator<PatcherViewModel>().selectedApp != null) {
onPressed: () { locator<PatcherViewModel>().loadLastSelectedPatches();
_managerAPI.setPatchesRepo(''); }
_managerAPI.setIntegrationsRepo(''); locator<Toast>().showBottom('settingsView.importedPatches');
Navigator.of(context).pop(); }
Navigator.of(context).pop(); } on Exception catch (e, s) {
}, await Sentry.captureException(e, stackTrace: s);
) locator<Toast>().showBottom('settingsView.jsonSelectorErrorMessage');
], }
),
);
} }
Future<void> showApiUrlResetDialog(BuildContext context) async { void resetSelectedPatches() {
return showDialog( _managerAPI.resetLastSelectedPatches();
context: context, _toast.showBottom('settingsView.resetStoredPatches');
builder: (context) => AlertDialog(
title: I18nText('settingsView.sourcesResetDialogTitle'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText('settingsView.apiURLResetDialogText'),
actions: <Widget>[
CustomMaterialButton(
isFilled: false,
label: I18nText('noButton'),
onPressed: () => Navigator.of(context).pop(),
),
CustomMaterialButton(
label: I18nText('yesButton'),
onPressed: () {
_managerAPI.setApiUrl('');
Navigator.of(context).pop();
Navigator.of(context).pop();
},
)
],
),
);
} }
Future<int> getSdkVersion() async { Future<int> getSdkVersion() async {
@@ -318,6 +115,15 @@ class SettingsViewModel extends BaseViewModel {
return info.version.sdkInt ?? -1; return info.version.sdkInt ?? -1;
} }
Future<void> deleteLogs() async {
Directory appCacheDir = await getTemporaryDirectory();
Directory logsDir = Directory('${appCacheDir.path}/logs');
if (logsDir.existsSync()) {
logsDir.deleteSync(recursive: true);
}
_toast.showBottom('settingsView.deletedLogs');
}
Future<void> exportLogcatLogs() async { Future<void> exportLogcatLogs() async {
Directory appCache = await getTemporaryDirectory(); Directory appCache = await getTemporaryDirectory();
Directory logDir = Directory('${appCache.path}/logs'); Directory logDir = Directory('${appCache.path}/logs');

View File

@@ -160,8 +160,9 @@ class AppInfoView extends StatelessWidget {
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
onTap: () { onTap: () {
model.navigateToPatcher(app); model.updateNotImplemented(context);
Navigator.of(context).pop(); // model.navigateToPatcher(app);
// Navigator.of(context).pop();
}, },
child: Column( child: Column(
mainAxisAlignment: mainAxisAlignment:
@@ -260,6 +261,22 @@ class AppInfoView extends StatelessWidget {
subtitle: Text(app.packageName), subtitle: Text(app.packageName),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'appInfoView.originalPackageNameLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: Text(app.originalPackageName),
),
const SizedBox(height: 4),
ListTile( ListTile(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 20.0), const EdgeInsets.symmetric(horizontal: 20.0),

View File

@@ -8,6 +8,7 @@ import 'package:revanced_manager/models/patched_application.dart';
import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/patcher_api.dart';
import 'package:revanced_manager/services/root_api.dart'; import 'package:revanced_manager/services/root_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; import 'package:revanced_manager/ui/views/home/home_viewmodel.dart';
import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart';
import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart';
@@ -19,6 +20,7 @@ class AppInfoViewModel extends BaseViewModel {
final ManagerAPI _managerAPI = locator<ManagerAPI>(); final ManagerAPI _managerAPI = locator<ManagerAPI>();
final PatcherAPI _patcherAPI = locator<PatcherAPI>(); final PatcherAPI _patcherAPI = locator<PatcherAPI>();
final RootAPI _rootAPI = RootAPI(); final RootAPI _rootAPI = RootAPI();
final Toast _toast = locator<Toast>();
Future<void> uninstallApp( Future<void> uninstallApp(
BuildContext context, BuildContext context,
@@ -51,6 +53,10 @@ class AppInfoViewModel extends BaseViewModel {
locator<NavigationViewModel>().setIndex(1); locator<NavigationViewModel>().setIndex(1);
} }
void updateNotImplemented(BuildContext context) {
_toast.showBottom('appInfoView.updateNotImplemented');
}
Future<void> showUninstallDialog( Future<void> showUninstallDialog(
BuildContext context, BuildContext context,
PatchedApplication app, PatchedApplication app,
@@ -127,7 +133,8 @@ class AppInfoViewModel extends BaseViewModel {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('appInfoView.appliedPatchesLabel'), title: I18nText('appInfoView.appliedPatchesLabel'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: Text(getAppliedPatchesString(app.appliedPatches)), content: SingleChildScrollView(
child: Text(getAppliedPatchesString(app.appliedPatches))),
actions: <Widget>[ actions: <Widget>[
CustomMaterialButton( CustomMaterialButton(
label: I18nText('okButton'), label: I18nText('okButton'),

View File

@@ -33,7 +33,7 @@ class _ContributorsCardState extends State<ContributorsCard> {
'', '',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w500,
), ),
), ),
), ),

View File

@@ -14,48 +14,74 @@ class AvailableUpdatesCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return apps.isEmpty return CustomCard(
? CustomCard( child: Center(
child: Center( child: Column(
child: Column( children: <Widget>[
children: <Widget>[ Icon(
Icon( size: 40,
size: 40, Icons.update_disabled,
Icons.update_disabled, color: Theme.of(context).colorScheme.secondary,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 16),
I18nText(
'homeView.noUpdates',
child: Text(
'',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
)
],
),
), ),
) const SizedBox(height: 16),
: ListView( I18nText(
shrinkWrap: true, 'homeView.WIP',
padding: EdgeInsets.zero, child: Text(
physics: const NeverScrollableScrollPhysics(), '',
children: apps textAlign: TextAlign.center,
.map((app) => ApplicationItem( style: Theme.of(context).textTheme.subtitle1!.copyWith(
icon: app.icon, color: Theme.of(context).colorScheme.secondary,
name: app.name, ),
patchDate: app.patchDate, ),
changelog: app.changelog, )
isUpdatableApp: true, ],
onPressed: () => ),
locator<HomeViewModel>().navigateToPatcher( ),
app, );
), // return apps.isEmpty
)) // ? CustomCard(
.toList(), // child: Center(
); // child: Column(
// children: <Widget>[
// Icon(
// size: 40,
// Icons.update_disabled,
// color: Theme.of(context).colorScheme.secondary,
// ),
// const SizedBox(height: 16),
// I18nText(
// 'homeView.noUpdates',
// child: Text(
// '',
// textAlign: TextAlign.center,
// style: Theme.of(context).textTheme.subtitle1!.copyWith(
// color: Theme.of(context).colorScheme.secondary,
// ),
// ),
// )
// ],
// ),
// ),
// )
// : ListView(
// shrinkWrap: true,
// padding: EdgeInsets.zero,
// physics: const NeverScrollableScrollPhysics(),
// children: apps
// .map((app) => ApplicationItem(
// icon: app.icon,
// name: app.name,
// patchDate: app.patchDate,
// changelog: app.changelog,
// isUpdatableApp: true,
// //TODO: Find a better way to do update functionality
// onPressed: () {}
// // () =>
// // locator<HomeViewModel>().navigateToPatcher(
// // app,
// // ),
// ))
// .toList(),
// );
} }
} }

View File

@@ -49,7 +49,7 @@ class _LatestCommitCardState extends State<LatestCommitCard> {
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 4),
Row( Row(
children: <Widget>[ children: <Widget>[
I18nText('latestCommitCard.managerLabel'), I18nText('latestCommitCard.managerLabel'),

View File

@@ -32,7 +32,7 @@ class AppSelectorCard extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 8),
locator<PatcherViewModel>().selectedApp == null locator<PatcherViewModel>().selectedApp == null
? I18nText('appSelectorCard.widgetSubtitle') ? I18nText('appSelectorCard.widgetSubtitle')
: Row( : Row(
@@ -49,21 +49,21 @@ class AppSelectorCard extends StatelessWidget {
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text(locator<PatcherViewModel>().getAppSelectionString()), Text(
locator<PatcherViewModel>()
.getAppSelectionString(),
style: const TextStyle(fontWeight: FontWeight.w600),
),
], ],
), ),
locator<PatcherViewModel>().selectedApp == null locator<PatcherViewModel>().selectedApp == null
? Container() ? Container()
: Column( : Column(
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 4),
Padding( Text(
padding: const EdgeInsets.only(left: 20), locator<PatcherViewModel>()
child: Text( .getRecommendedVersionString(context),
locator<PatcherViewModel>()
.getRecommendedVersionString(context),
style: const TextStyle(fontStyle: FontStyle.italic),
),
), ),
], ],
), ),

View File

@@ -20,19 +20,32 @@ class PatchSelectorCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
I18nText( Row(
locator<PatcherViewModel>().selectedPatches.isEmpty children: <Widget>[
? 'patchSelectorCard.widgetTitle' I18nText(
: 'patchSelectorCard.widgetTitleSelected', locator<PatcherViewModel>().selectedPatches.isEmpty
child: const Text( ? 'patchSelectorCard.widgetTitle'
'', : 'patchSelectorCard.widgetTitleSelected',
style: TextStyle( child: const Text(
fontSize: 18, '',
fontWeight: FontWeight.w500, style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
), ),
), Text(
locator<PatcherViewModel>().selectedPatches.isEmpty
? ''
: ' (${locator<PatcherViewModel>().selectedPatches.length})',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
],
), ),
const SizedBox(height: 10), const SizedBox(height: 4),
locator<PatcherViewModel>().selectedApp == null locator<PatcherViewModel>().selectedApp == null
? I18nText('patchSelectorCard.widgetSubtitle') ? I18nText('patchSelectorCard.widgetSubtitle')
: locator<PatcherViewModel>().selectedPatches.isEmpty : locator<PatcherViewModel>().selectedPatches.isEmpty
@@ -46,7 +59,7 @@ class PatchSelectorCard extends StatelessWidget {
String _getPatchesSelection() { String _getPatchesSelection() {
String text = ''; String text = '';
for (Patch p in locator<PatcherViewModel>().selectedPatches) { for (Patch p in locator<PatcherViewModel>().selectedPatches) {
text += '${p.getSimpleName()} (v${p.version})\n'; text += '\u2022 ${p.getSimpleName()} (v${p.version})\n';
} }
return text.substring(0, text.length - 1); return text.substring(0, text.length - 1);
} }

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:revanced_manager/app/app.locator.dart';
import 'package:revanced_manager/services/manager_api.dart';
import 'package:revanced_manager/services/toast.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart';
import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_card.dart';
@@ -15,6 +18,8 @@ class PatchItem extends StatefulWidget {
bool isSelected; bool isSelected;
final Function(bool) onChanged; final Function(bool) onChanged;
final Widget? child; final Widget? child;
final toast = locator<Toast>();
final _managerAPI = locator<ManagerAPI>();
PatchItem( PatchItem(
{Key? key, {Key? key,
@@ -40,8 +45,22 @@ class _PatchItemState extends State<PatchItem> {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: CustomCard( child: CustomCard(
backgroundColor: widget.isUnsupported &&
widget._managerAPI.areExperimentalPatchesEnabled() == false
? Theme.of(context).colorScheme.brightness == Brightness.light
? Colors.grey[400]
: Colors.grey[700]
: null,
onTap: () { onTap: () {
setState(() => widget.isSelected = !widget.isSelected); setState(() {
if (widget.isUnsupported &&
!widget._managerAPI.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast.showBottom('patchItem.unsupportedPatchVersion');
} else {
widget.isSelected = !widget.isSelected;
}
});
widget.onChanged(widget.isSelected); widget.onChanged(widget.isSelected);
}, },
child: Column( child: Column(
@@ -58,13 +77,21 @@ class _PatchItemState extends State<PatchItem> {
children: <Widget>[ children: <Widget>[
Text( Text(
widget.simpleName, widget.simpleName,
maxLines: 2,
overflow: TextOverflow.visible,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 6),
Text(widget.version) Text(
widget.version,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.secondary,
),
),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -73,7 +100,12 @@ class _PatchItemState extends State<PatchItem> {
softWrap: true, softWrap: true,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
style: const TextStyle(fontSize: 14), style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
), ),
], ],
), ),
@@ -90,7 +122,17 @@ class _PatchItemState extends State<PatchItem> {
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
onChanged: (newValue) { onChanged: (newValue) {
setState(() => widget.isSelected = newValue!); setState(() {
if (widget.isUnsupported &&
!widget._managerAPI
.areExperimentalPatchesEnabled()) {
widget.isSelected = false;
widget.toast
.showBottom('patchItem.unsupportedPatchVersion');
} else {
widget.isSelected = newValue!;
}
});
widget.onChanged(widget.isSelected); widget.onChanged(widget.isSelected);
}, },
), ),
@@ -103,13 +145,13 @@ class _PatchItemState extends State<PatchItem> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: TextButton.icon( child: TextButton.icon(
label: I18nText('patchItem.unsupportedWarningButton'), label: I18nText('warning'),
icon: const Icon(Icons.warning), icon: const Icon(Icons.warning, size: 20.0),
onPressed: () => _showUnsupportedWarningDialog(), onPressed: () => _showUnsupportedWarningDialog(),
style: ButtonStyle( style: ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(8),
side: BorderSide( side: BorderSide(
width: 1, width: 1,
color: color:
@@ -140,7 +182,7 @@ class _PatchItemState extends State<PatchItem> {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: I18nText('patchItem.unsupportedDialogTitle'), title: I18nText('warning'),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
content: I18nText( content: I18nText(
'patchItem.unsupportedDialogText', 'patchItem.unsupportedDialogText',

View File

@@ -0,0 +1,72 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_manage_api_url.dart';
import 'package:revanced_manager/ui/views/settings/settingsFragement/settings_manage_sources.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SAdvancedSection extends StatelessWidget {
const SAdvancedSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.advancedSectionTitle',
children: <Widget>[
SManageApiUrlUI(),
SManageSourcesUI(),
SExperimentalPatches(),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteKeystoreLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteKeystoreHint'),
onTap: () => _settingsViewModel.deleteKeystore,
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteTempDirLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteTempDirHint'),
onTap: () => _settingsViewModel.deleteTempDir(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.deleteLogsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.deleteLogsHint'),
onTap: () => _settingsViewModel.deleteLogs(),
),
],
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
class SExperimentalPatches extends StatefulWidget {
const SExperimentalPatches({super.key});
@override
State<SExperimentalPatches> createState() => _SExperimentalPatchesState();
}
final _settingsViewModel = SettingsViewModel();
class _SExperimentalPatchesState extends State<SExperimentalPatches> {
@override
Widget build(BuildContext context) {
return CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.experimentalPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.experimentalPatchesHint'),
value: _settingsViewModel.areExperimentalPatchesEnabled(),
onTap: (value) {
setState(() {
_settingsViewModel.useExperimentalPatches(value);
});
},
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SExportSection extends StatelessWidget {
const SExportSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.exportSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.exportPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.exportPatchesHint'),
onTap: () => _settingsViewModel.exportPatches(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.importPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.importPatchesHint'),
onTap: () => _settingsViewModel.importPatches(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.resetStoredPatchesLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.resetStoredPatchesHint'),
onTap: () => _settingsViewModel.resetSelectedPatches(),
),
],
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SInfoSection extends StatelessWidget {
const SInfoSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.infoSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.logsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.logsHint'),
onTap: () => _settingsViewModel.exportLogcatLogs(),
),
const AboutWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/custom_switch_tile.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
final _settingsViewModel = SettingsViewModel();
class SLoggingSection extends StatelessWidget {
const SLoggingSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.logsSectionTitle',
children: <Widget>[
CustomSwitchTile(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.sentryLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.sentryHint'),
value: _settingsViewModel.isSentryEnabled(),
onTap: (value) => _settingsViewModel.useSentry(value),
),
],
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_i18n/widgets/I18nText.dart';
import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart';
import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart';
import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart';
final _settingsViewModel = SettingsViewModel();
class STeamSection extends StatelessWidget {
const STeamSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: 'settingsView.teamSectionTitle',
children: <Widget>[
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20.0),
title: I18nText(
'settingsView.contributorsLabel',
child: const Text(
'',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
subtitle: I18nText('settingsView.contributorsHint'),
onTap: () => _settingsViewModel.navigateToContributors(),
),
const SocialMediaWidget(
padding: EdgeInsets.symmetric(horizontal: 20.0),
),
],
);
}
}

View File

@@ -50,18 +50,8 @@ class _ApplicationItemState extends State<ApplicationItem>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ExpandableController expController = ExpandableController(); ExpandableController expController = ExpandableController();
return ExpandablePanel( return Container(
controller: expController, margin: const EdgeInsets.only(bottom: 16.0),
theme: const ExpandableThemeData(
inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
hasIcon: false,
animationDuration: Duration(milliseconds: 450),
),
header: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: CustomCard( child: CustomCard(
onTap: () { onTap: () {
expController.toggle(); expController.toggle();
@@ -69,74 +59,85 @@ class _ApplicationItemState extends State<ApplicationItem>
? _animationController.reverse() ? _animationController.reverse()
: _animationController.forward(); : _animationController.forward();
}, },
child: Row( child: ExpandablePanel(
children: <Widget>[ controller: expController,
SizedBox( theme: const ExpandableThemeData(
width: 40, inkWellBorderRadius: BorderRadius.all(Radius.circular(16)),
child: Image.memory(widget.icon, height: 40, width: 40), tapBodyToCollapse: false,
), tapBodyToExpand: false,
const SizedBox(width: 4), tapHeaderToExpand: false,
Padding( hasIcon: false,
padding: const EdgeInsets.only(left: 15.0), animationDuration: Duration(milliseconds: 450),
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, header: Row(
children: <Widget>[ children: <Widget>[
Text( SizedBox(
widget.name.length > 9 width: 40,
? '${widget.name.substring(0, 9)}...' child: Image.memory(widget.icon, height: 40, width: 40),
: widget.name, ),
style: const TextStyle( const SizedBox(width: 4),
fontSize: 16, Padding(
fontWeight: FontWeight.w500, padding: const EdgeInsets.only(left: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.name.length > 12
? '${widget.name.substring(0, 12)}...'
: widget.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
), ),
Text(format(widget.patchDate)),
],
),
),
const Spacer(),
RotationTransition(
turns: Tween(begin: 0.0, end: 0.50)
.animate(_animationController),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.arrow_drop_down),
),
),
const SizedBox(width: 8),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CustomMaterialButton(
label: widget.isUpdatableApp
? I18nText('applicationItem.patchButton')
: I18nText('applicationItem.infoButton'),
onPressed: widget.onPressed,
), ),
Text(format(widget.patchDate)),
], ],
), ),
), ],
const Spacer(), ),
RotationTransition( collapsed: const SizedBox(),
turns: expanded: Padding(
Tween(begin: 0.0, end: 0.50).animate(_animationController), padding: const EdgeInsets.only(
child: const Padding( top: 16.0, left: 4.0, right: 4.0, bottom: 4.0),
padding: EdgeInsets.all(8.0), child: Column(
child: Icon(Icons.arrow_drop_down), crossAxisAlignment: CrossAxisAlignment.start,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
CustomMaterialButton( I18nText(
label: widget.isUpdatableApp 'applicationItem.changelogLabel',
? I18nText('applicationItem.patchButton') child: const Text(
: I18nText('applicationItem.infoButton'), '',
onPressed: widget.onPressed, style: TextStyle(fontWeight: FontWeight.w700),
),
), ),
const SizedBox(height: 4),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'),
], ],
), ),
],
),
),
),
collapsed: const SizedBox(),
expanded: Padding(
padding: const EdgeInsets.all(16.0).copyWith(top: 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
I18nText(
'applicationItem.changelogLabel',
child: const Text(
'',
style: TextStyle(fontWeight: FontWeight.w700),
),
), ),
const SizedBox(height: 4), ),
Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'), ));
],
),
),
);
} }
} }

View File

@@ -5,22 +5,25 @@ class CustomCard extends StatelessWidget {
final Widget child; final Widget child;
final Function()? onTap; final Function()? onTap;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final Color? backgroundColor;
const CustomCard({ const CustomCard(
Key? key, {Key? key,
this.isFilled = true, this.isFilled = true,
required this.child, required this.child,
this.onTap, this.onTap,
this.padding, this.padding,
}) : super(key: key); this.backgroundColor})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
type: isFilled ? MaterialType.card : MaterialType.transparency, type: isFilled ? MaterialType.card : MaterialType.transparency,
color: isFilled color: isFilled
? Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4) ? backgroundColor?.withOpacity(0.4) ??
: Colors.transparent, Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4)
: backgroundColor ?? Colors.transparent,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DashboardChip extends StatelessWidget { class CustomChip extends StatelessWidget {
final Widget label; final Widget label;
final bool isSelected; final bool isSelected;
final Function(bool)? onSelected; final Function(bool)? onSelected;
const DashboardChip({ const CustomChip({
Key? key, Key? key,
required this.label, required this.label,
required this.isSelected, this.isSelected = false,
this.onSelected, this.onSelected,
}) : super(key: key); }) : super(key: key);

View File

@@ -0,0 +1,10 @@
// Check for google mobile services on device
import 'package:device_apps/device_apps.dart';
Future<bool> checkForGMS() async {
bool isGMSInstalled = true;
isGMSInstalled = await DeviceApps.isAppInstalled('com.google.android.gms') ||
await DeviceApps.isAppInstalled('com.android.vending');
return isGMSInstalled;
}

View File

@@ -0,0 +1,5 @@
// Dummy environment variables used for building the app locally. These automatically get set with correct values during workflow builds.
class Environment {
static const sentryDSN = '';
static const crowdinKEY = '';
}

View File

@@ -4,23 +4,25 @@ homepage: https://github.com/revanced/revanced-manager
publish_to: 'none' publish_to: 'none'
version: 0.0.22+22 version: 0.0.41+41
environment: environment:
sdk: ">=2.17.5 <3.0.0" sdk: ">=2.17.5 <3.0.0"
dependencies: dependencies:
sentry_flutter: ^6.12.2
animations: ^2.0.4 animations: ^2.0.4
app_installer: ^1.1.0 app_installer: ^1.1.0
collection: ^1.16.0 collection: ^1.16.0
cross_connectivity: ^3.0.5 cross_connectivity: ^3.0.5
cr_file_saver: ^0.0.1+2
device_apps: device_apps:
git: git:
url: https://github.com/ponces/flutter_plugin_device_apps url: https://github.com/ponces/flutter_plugin_device_apps
ref: revanced-manager ref: revanced-manager
device_info_plus: ^4.1.2 device_info_plus: ^4.1.2
dio: ^4.0.6 dio: ^4.0.6
dio_http2_adapter: ^2.0.0 dio_brotli_transformer: ^1.0.1
dio_http_cache_lts: ^0.4.1 dio_http_cache_lts: ^0.4.1
dynamic_color: ^1.5.4 dynamic_color: ^1.5.4
dynamic_themes: ^1.1.0 dynamic_themes: ^1.1.0
@@ -51,6 +53,7 @@ dependencies:
git: git:
url: https://github.com/SuaMusica/logcat url: https://github.com/SuaMusica/logcat
ref: feature/nullSafe ref: feature/nullSafe
native_dio_client: ^0.0.1-dev+1
package_info_plus: ^1.4.3+1 package_info_plus: ^1.4.3+1
path_provider: ^2.0.11 path_provider: ^2.0.11
permission_handler: ^10.0.0 permission_handler: ^10.0.0
@@ -70,17 +73,22 @@ dependencies:
timezone: ^0.8.0 timezone: ^0.8.0
url_launcher: ^6.1.5 url_launcher: ^6.1.5
wakelock: ^0.6.2 wakelock: ^0.6.2
sentry_dio: ^6.12.2
flutter_dotenv: ^5.0.2
dev_dependencies: dev_dependencies:
json_serializable: ^6.3.1
build_runner: any build_runner: any
flutter_launcher_icons: ^0.10.0 flutter_launcher_icons: ^0.10.0
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1
flutter_test: flutter_test:
sdk: flutter sdk: flutter
injectable_generator: ^1.5.4 injectable_generator: ^1.5.4
json_serializable: ^6.3.1
flutter: flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/i18n/ - assets/i18n/
- .env